Add local iOS release pipeline, fix shortcuts, and improve invoice UX.
Enable App Store builds without EAS, iOS 18 App Intents plugins, and signing fixes for distribution export. Add mobile invoice PDF preview, compact line items, and more reliable shortcut deep-link handling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+36
-72
@@ -1,7 +1,14 @@
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Alert, Platform, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import {
|
||||
InvoiceEditorSectionTabs,
|
||||
type InvoiceEditorSection,
|
||||
} from "@/components/invoices/InvoiceEditorSectionTabs";
|
||||
import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview";
|
||||
import { InvoiceTotals } from "@/components/invoices/InvoiceTotals";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
@@ -12,6 +19,7 @@ import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import type { ThemeColors } from "@/lib/theme-palette";
|
||||
import { useThemedStyles } from "@/lib/use-themed-styles";
|
||||
import { getInvoiceStatus, type InvoiceStatus } from "@/lib/invoice-status";
|
||||
import { buildPreviewPdfInputFromInvoice } from "@/lib/invoice-pdf-input";
|
||||
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
|
||||
import { api } from "@/lib/trpc";
|
||||
|
||||
@@ -21,6 +29,7 @@ export default function InvoiceDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const utils = api.useUtils();
|
||||
const scrollPadding = useTabBarScrollPadding();
|
||||
const [section, setSection] = useState<InvoiceEditorSection>("edit");
|
||||
|
||||
const invoiceQuery = api.invoices.getById.useQuery(
|
||||
{ id: id ?? "" },
|
||||
@@ -81,6 +90,10 @@ export default function InvoiceDetailScreen() {
|
||||
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const taxAmount = subtotal * (invoice.taxRate / 100);
|
||||
const clientEmail = invoice.client?.email?.trim() ?? "";
|
||||
const previewInput = useMemo(
|
||||
() => buildPreviewPdfInputFromInvoice(invoice),
|
||||
[invoice],
|
||||
);
|
||||
|
||||
function promptSendInvoice() {
|
||||
if (!clientEmail) {
|
||||
@@ -188,6 +201,19 @@ export default function InvoiceDetailScreen() {
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
<InvoiceEditorSectionTabs
|
||||
value={section}
|
||||
onChange={setSection}
|
||||
editLabel="Details"
|
||||
previewLabel="PDF"
|
||||
/>
|
||||
|
||||
{section === "preview" ? (
|
||||
<Card title="PDF preview">
|
||||
<InvoicePdfPreview input={previewInput} />
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card title="Details">
|
||||
<DetailRow label="Issued" value={formatDate(invoice.issueDate)} />
|
||||
<DetailRow label="Due" value={formatDate(invoice.dueDate)} />
|
||||
@@ -222,20 +248,14 @@ export default function InvoiceDetailScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.totals}>
|
||||
<TotalRow label="Subtotal" value={formatCurrency(subtotal, invoice.currency)} />
|
||||
{invoice.taxRate > 0 ? (
|
||||
<TotalRow
|
||||
label={`Tax (${invoice.taxRate}%)`}
|
||||
value={formatCurrency(taxAmount, invoice.currency)}
|
||||
/>
|
||||
) : null}
|
||||
<TotalRow
|
||||
label="Total"
|
||||
value={formatCurrency(invoice.totalAmount, invoice.currency)}
|
||||
bold
|
||||
/>
|
||||
</View>
|
||||
<InvoiceTotals
|
||||
subtotal={formatCurrency(subtotal, invoice.currency)}
|
||||
taxLabel={invoice.taxRate > 0 ? `Tax (${invoice.taxRate}%)` : undefined}
|
||||
taxAmount={
|
||||
invoice.taxRate > 0 ? formatCurrency(taxAmount, invoice.currency) : undefined
|
||||
}
|
||||
total={formatCurrency(invoice.totalAmount, invoice.currency)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{invoice.notes ? (
|
||||
@@ -281,6 +301,8 @@ export default function InvoiceDetailScreen() {
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</AppBackground>
|
||||
);
|
||||
@@ -296,41 +318,6 @@ function DetailRow({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TotalRow({
|
||||
label,
|
||||
value,
|
||||
bold,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
bold?: boolean;
|
||||
}) {
|
||||
const { colors } = useAppTheme();
|
||||
return (
|
||||
<View style={detailStyles.totalRow}>
|
||||
<Text
|
||||
style={[
|
||||
detailStyles.totalLabel,
|
||||
{ color: colors.mutedForeground },
|
||||
bold && detailStyles.totalBold,
|
||||
bold && { color: colors.foreground },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
detailStyles.totalValue,
|
||||
{ color: colors.foreground },
|
||||
bold && detailStyles.totalBold,
|
||||
]}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const detailStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
@@ -346,22 +333,6 @@ const detailStyles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.bodyMedium,
|
||||
},
|
||||
totalRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
totalLabel: {
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14,
|
||||
},
|
||||
totalValue: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 14,
|
||||
},
|
||||
totalBold: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
const createInvoiceDetailStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
@@ -427,13 +398,6 @@ const createInvoiceDetailStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
color: colors.foreground,
|
||||
fontSize: 14,
|
||||
},
|
||||
totals: {
|
||||
marginTop: spacing.sm,
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
gap: 4,
|
||||
},
|
||||
notes: {
|
||||
fontFamily: fonts.body,
|
||||
color: colors.foreground,
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
} from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LineItemEditor, type EditableLineItem } from "@/components/invoices/LineItemEditor";
|
||||
import {
|
||||
InvoiceEditorSectionTabs,
|
||||
type InvoiceEditorSection,
|
||||
} from "@/components/invoices/InvoiceEditorSectionTabs";
|
||||
import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview";
|
||||
import { InvoiceTotals } from "@/components/invoices/InvoiceTotals";
|
||||
import { LineItemEditor, LineItemsTableHeader, type EditableLineItem } from "@/components/invoices/LineItemEditor";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
@@ -22,6 +28,7 @@ import { fonts, spacing } from "@/constants/theme";
|
||||
import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { getInvoiceStatus } from "@/lib/invoice-status";
|
||||
import { buildPreviewPdfInput } from "@/lib/invoice-pdf-input";
|
||||
import { validateLineItems } from "@/lib/form-validation";
|
||||
import { ensureNotificationPermissions } from "@/lib/invoice-send-reminders";
|
||||
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
|
||||
@@ -45,7 +52,7 @@ export default function InvoiceEditScreen() {
|
||||
const [dueDate, setDueDate] = useState(() => new Date());
|
||||
const [sendReminderAt, setSendReminderAt] = useState<Date | null>(null);
|
||||
const [items, setItems] = useState<EditableLineItem[]>([]);
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||
const [section, setSection] = useState<InvoiceEditorSection>("edit");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,7 +70,6 @@ export default function InvoiceEditScreen() {
|
||||
rate: String(item.rate),
|
||||
})),
|
||||
);
|
||||
setExpandedIndex(null);
|
||||
}, [invoiceQuery.data]);
|
||||
|
||||
const updateInvoice = api.invoices.update.useMutation({
|
||||
@@ -109,6 +115,23 @@ export default function InvoiceEditScreen() {
|
||||
const lineItemsError = isDraft ? validateLineItems(items) : null;
|
||||
const canSave = isDraft ? !lineItemsError : true;
|
||||
|
||||
const previewInput = useMemo(() => {
|
||||
if (!invoice) return null;
|
||||
return buildPreviewPdfInput({
|
||||
invoiceNumber: invoice.invoiceNumber,
|
||||
invoicePrefix: invoice.invoicePrefix,
|
||||
businessId: invoice.businessId,
|
||||
clientId: invoice.clientId,
|
||||
issueDate: new Date(invoice.issueDate),
|
||||
dueDate,
|
||||
status: invoice.status as "draft" | "sent" | "paid",
|
||||
notes,
|
||||
taxRate,
|
||||
currency,
|
||||
items,
|
||||
});
|
||||
}, [invoice, dueDate, notes, taxRate, currency, items]);
|
||||
|
||||
if (!id) {
|
||||
return <LoadingScreen message="Invalid invoice" />;
|
||||
}
|
||||
@@ -151,7 +174,6 @@ export default function InvoiceEditScreen() {
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
const nextIndex = items.length;
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -161,7 +183,6 @@ export default function InvoiceEditScreen() {
|
||||
rate: prev[prev.length - 1]?.rate ?? "0",
|
||||
},
|
||||
]);
|
||||
setExpandedIndex(nextIndex);
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
@@ -170,11 +191,6 @@ export default function InvoiceEditScreen() {
|
||||
return;
|
||||
}
|
||||
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||
setExpandedIndex((current) => {
|
||||
if (current === null) return null;
|
||||
if (current === index) return null;
|
||||
return current > index ? current - 1 : current;
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -241,6 +257,14 @@ export default function InvoiceEditScreen() {
|
||||
<Text style={styles.clientName}>{invoice.client?.name ?? "Client"}</Text>
|
||||
</View>
|
||||
|
||||
<InvoiceEditorSectionTabs value={section} onChange={setSection} />
|
||||
|
||||
{section === "preview" ? (
|
||||
<Card title="PDF preview">
|
||||
<InvoicePdfPreview input={previewInput} />
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card>
|
||||
<DateTimeField label="Due date" mode="date" value={dueDate} onChange={setDueDate} />
|
||||
{isDraft ? (
|
||||
@@ -278,16 +302,16 @@ export default function InvoiceEditScreen() {
|
||||
Line items are locked after an invoice is sent. Mark as draft on the invoice
|
||||
screen to edit entries.
|
||||
</Text>
|
||||
) : null}
|
||||
) : (
|
||||
<LineItemsTableHeader />
|
||||
)}
|
||||
{items.map((item, index) => (
|
||||
<LineItemEditor
|
||||
key={item.id ?? `new-${index}`}
|
||||
index={index}
|
||||
item={item}
|
||||
currency={currency}
|
||||
expanded={expandedIndex === index}
|
||||
onToggle={() =>
|
||||
setExpandedIndex((current) => (current === index ? null : index))
|
||||
}
|
||||
isLast={index === items.length - 1}
|
||||
onChange={(patch) => updateItem(index, patch)}
|
||||
onRemove={() => removeItem(index)}
|
||||
readOnly={!isDraft}
|
||||
@@ -300,16 +324,12 @@ export default function InvoiceEditScreen() {
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<View style={styles.totals}>
|
||||
<TotalRow label="Subtotal" value={formatCurrency(subtotal, currency)} />
|
||||
{taxRate > 0 ? (
|
||||
<TotalRow
|
||||
label={`Tax (${taxRate}%)`}
|
||||
value={formatCurrency(taxAmount, currency)}
|
||||
/>
|
||||
) : null}
|
||||
<TotalRow label="Total" value={formatCurrency(total, currency)} bold />
|
||||
</View>
|
||||
<InvoiceTotals
|
||||
subtotal={formatCurrency(subtotal, currency)}
|
||||
taxLabel={taxRate > 0 ? `Tax (${taxRate}%)` : undefined}
|
||||
taxAmount={taxRate > 0 ? formatCurrency(taxAmount, currency) : undefined}
|
||||
total={formatCurrency(total, currency)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{lineItemsError ? <Text style={styles.error}>{lineItemsError}</Text> : null}
|
||||
@@ -331,67 +351,14 @@ export default function InvoiceEditScreen() {
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</AppBackground>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalRow({
|
||||
label,
|
||||
value,
|
||||
bold,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
bold?: boolean;
|
||||
}) {
|
||||
const { colors } = useAppTheme();
|
||||
return (
|
||||
<View style={totalStyles.row}>
|
||||
<Text
|
||||
style={[
|
||||
totalStyles.label,
|
||||
{ color: colors.mutedForeground },
|
||||
bold && totalStyles.bold,
|
||||
bold && { color: colors.foreground },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
totalStyles.value,
|
||||
{ color: colors.foreground },
|
||||
bold && totalStyles.bold,
|
||||
]}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const totalStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
label: {
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14,
|
||||
},
|
||||
value: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 14,
|
||||
},
|
||||
bold: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const createInvoiceEditStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
@@ -437,13 +404,6 @@ const createInvoiceEditStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
fontSize: 14,
|
||||
color: colors.primary,
|
||||
},
|
||||
totals: {
|
||||
marginTop: spacing.sm,
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
gap: 6,
|
||||
},
|
||||
error: {
|
||||
color: colors.destructive,
|
||||
fontFamily: fonts.body,
|
||||
|
||||
+45
-85
@@ -12,7 +12,13 @@ import {
|
||||
} from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LineItemEditor, type EditableLineItem } from "@/components/invoices/LineItemEditor";
|
||||
import {
|
||||
InvoiceEditorSectionTabs,
|
||||
type InvoiceEditorSection,
|
||||
} from "@/components/invoices/InvoiceEditorSectionTabs";
|
||||
import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview";
|
||||
import { InvoiceTotals } from "@/components/invoices/InvoiceTotals";
|
||||
import { LineItemEditor, LineItemsTableHeader, type EditableLineItem } from "@/components/invoices/LineItemEditor";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
@@ -23,6 +29,7 @@ import { fonts, spacing } from "@/constants/theme";
|
||||
import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { defaultDueDate, generateInvoiceNumber } from "@/lib/invoice-number";
|
||||
import { buildPreviewPdfInput } from "@/lib/invoice-pdf-input";
|
||||
import {
|
||||
isRequiredString,
|
||||
isValidTaxRate,
|
||||
@@ -55,7 +62,7 @@ export default function NewInvoiceScreen() {
|
||||
rate: "0",
|
||||
},
|
||||
]);
|
||||
const [expandedIndex, setExpandedIndex] = useState<number | null>(0);
|
||||
const [section, setSection] = useState<InvoiceEditorSection>("edit");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const clientOptions = useMemo(
|
||||
@@ -109,6 +116,21 @@ export default function NewInvoiceScreen() {
|
||||
const taxAmount = subtotal * (parsedTaxRate / 100);
|
||||
const total = subtotal + taxAmount;
|
||||
|
||||
const previewInput = useMemo(
|
||||
() =>
|
||||
buildPreviewPdfInput({
|
||||
invoiceNumber,
|
||||
clientId,
|
||||
issueDate,
|
||||
dueDate,
|
||||
taxRate: parsedTaxRate,
|
||||
currency,
|
||||
notes,
|
||||
items,
|
||||
}),
|
||||
[invoiceNumber, clientId, issueDate, dueDate, parsedTaxRate, currency, notes, items],
|
||||
);
|
||||
|
||||
const clientError = clientId ? undefined : "Select a client";
|
||||
const invoiceNumberError = isRequiredString(invoiceNumber)
|
||||
? undefined
|
||||
@@ -131,7 +153,6 @@ export default function NewInvoiceScreen() {
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
const nextIndex = items.length;
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -141,7 +162,6 @@ export default function NewInvoiceScreen() {
|
||||
rate: prev[prev.length - 1]?.rate ?? "0",
|
||||
},
|
||||
]);
|
||||
setExpandedIndex(nextIndex);
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
@@ -150,11 +170,6 @@ export default function NewInvoiceScreen() {
|
||||
return;
|
||||
}
|
||||
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||
setExpandedIndex((current) => {
|
||||
if (current === null) return null;
|
||||
if (current === index) return null;
|
||||
return current > index ? current - 1 : current;
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
@@ -203,6 +218,14 @@ export default function NewInvoiceScreen() {
|
||||
scrollIndicatorInsets={{ bottom: scrollPadding }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<InvoiceEditorSectionTabs value={section} onChange={setSection} />
|
||||
|
||||
{section === "preview" ? (
|
||||
<Card title="PDF preview">
|
||||
<InvoicePdfPreview input={previewInput} />
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card title="Details">
|
||||
{clientOptions.length === 0 ? (
|
||||
<View style={styles.noClients}>
|
||||
@@ -262,15 +285,14 @@ export default function NewInvoiceScreen() {
|
||||
</Card>
|
||||
|
||||
<Card title="Line items">
|
||||
<LineItemsTableHeader />
|
||||
{items.map((item, index) => (
|
||||
<LineItemEditor
|
||||
key={`new-${index}`}
|
||||
index={index}
|
||||
item={item}
|
||||
currency={currency}
|
||||
expanded={expandedIndex === index}
|
||||
onToggle={() =>
|
||||
setExpandedIndex((current) => (current === index ? null : index))
|
||||
}
|
||||
isLast={index === items.length - 1}
|
||||
onChange={(patch) => updateItem(index, patch)}
|
||||
onRemove={() => removeItem(index)}
|
||||
/>
|
||||
@@ -280,16 +302,14 @@ export default function NewInvoiceScreen() {
|
||||
<Text style={styles.addLineText}>+ Add line</Text>
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.totals}>
|
||||
<TotalRow label="Subtotal" value={formatCurrency(subtotal, currency)} />
|
||||
{parsedTaxRate > 0 ? (
|
||||
<TotalRow
|
||||
label={`Tax (${parsedTaxRate}%)`}
|
||||
value={formatCurrency(taxAmount, currency)}
|
||||
/>
|
||||
) : null}
|
||||
<TotalRow label="Total" value={formatCurrency(total, currency)} bold />
|
||||
</View>
|
||||
<InvoiceTotals
|
||||
subtotal={formatCurrency(subtotal, currency)}
|
||||
taxLabel={parsedTaxRate > 0 ? `Tax (${parsedTaxRate}%)` : undefined}
|
||||
taxAmount={
|
||||
parsedTaxRate > 0 ? formatCurrency(taxAmount, currency) : undefined
|
||||
}
|
||||
total={formatCurrency(total, currency)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{lineItemsError ? <Text style={styles.error}>{lineItemsError}</Text> : null}
|
||||
@@ -301,67 +321,14 @@ export default function NewInvoiceScreen() {
|
||||
disabled={!canCreate}
|
||||
onPress={handleCreate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</AppBackground>
|
||||
);
|
||||
}
|
||||
|
||||
function TotalRow({
|
||||
label,
|
||||
value,
|
||||
bold,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
bold?: boolean;
|
||||
}) {
|
||||
const { colors } = useAppTheme();
|
||||
return (
|
||||
<View style={totalStyles.row}>
|
||||
<Text
|
||||
style={[
|
||||
totalStyles.label,
|
||||
{ color: colors.mutedForeground },
|
||||
bold && totalStyles.bold,
|
||||
bold && { color: colors.foreground },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
totalStyles.value,
|
||||
{ color: colors.foreground },
|
||||
bold && totalStyles.bold,
|
||||
]}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const totalStyles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
label: {
|
||||
fontFamily: fonts.body,
|
||||
fontSize: 14,
|
||||
},
|
||||
value: {
|
||||
fontFamily: fonts.bodyMedium,
|
||||
fontSize: 14,
|
||||
},
|
||||
bold: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const createNewInvoiceStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
@@ -391,13 +358,6 @@ const createNewInvoiceStyles = (colors: ThemeColors, _isDark: boolean) =>
|
||||
fontSize: 14,
|
||||
color: colors.primary,
|
||||
},
|
||||
totals: {
|
||||
marginTop: spacing.sm,
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
gap: 6,
|
||||
},
|
||||
error: {
|
||||
color: colors.destructive,
|
||||
fontFamily: fonts.body,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { BrandBackground } from "@/components/BrandBackground";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { SessionSync } from "@/components/SessionSync";
|
||||
import { ShortcutLinkCapture } from "@/components/ShortcutLinkCapture";
|
||||
import { AccountsProvider, useAccounts } from "@/contexts/AccountsContext";
|
||||
import { AuthProvider, useSession } from "@/contexts/AuthContext";
|
||||
import { ThemeProvider, useAppTheme } from "@/contexts/ThemeContext";
|
||||
@@ -36,6 +37,7 @@ function AppServices({ children }: { children: ReactNode }) {
|
||||
<AuthProvider apiUrl={apiUrl} storagePrefix={authStoragePrefix} key={remountKey}>
|
||||
<TRPCProvider apiUrl={apiUrl} key={remountKey}>
|
||||
<SessionSync />
|
||||
<ShortcutLinkCapture />
|
||||
{children}
|
||||
</TRPCProvider>
|
||||
</AuthProvider>
|
||||
|
||||
Reference in New Issue
Block a user