diff --git a/apps/mobile/app/(app)/(tabs)/index.tsx b/apps/mobile/app/(app)/(tabs)/index.tsx
index 60a99205..fbc52fea 100644
--- a/apps/mobile/app/(app)/(tabs)/index.tsx
+++ b/apps/mobile/app/(app)/(tabs)/index.tsx
@@ -135,11 +135,12 @@ export default function HomeScreen() {
)}
renderSectionHeader={({ section: { title, sum } }) => (
-
- {title}
+
+ {title}
{t(i18n)`Get 6pm Pro`}
-
+
{t(i18n)`Unlocks full AI power and more!`}
@@ -315,7 +315,7 @@ export default function SettingsScreen() {
className="!px-6 justify-start gap-6"
>
-
+
{t(i18n)`Sign out`}
diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx
index 4eb7a771..ddc25d1a 100644
--- a/apps/mobile/app/(app)/_layout.tsx
+++ b/apps/mobile/app/(app)/_layout.tsx
@@ -161,6 +161,13 @@ export default function AuthenticatedLayout() {
headerTitle: t(i18n)`New budget`,
}}
/>
+
)
diff --git a/apps/mobile/app/(app)/blob-viewer.tsx b/apps/mobile/app/(app)/blob-viewer.tsx
new file mode 100644
index 00000000..6f2c6507
--- /dev/null
+++ b/apps/mobile/app/(app)/blob-viewer.tsx
@@ -0,0 +1,18 @@
+import { useLocalSearchParams } from 'expo-router'
+import { Image, View } from 'react-native'
+
+export default function BlobViewerScreen() {
+ const { blobObjectUrl } = useLocalSearchParams()
+ if (!blobObjectUrl) {
+ return null
+ }
+ return (
+
+
+
+ )
+}
diff --git a/apps/mobile/app/(app)/budget/[budgetId]/index.tsx b/apps/mobile/app/(app)/budget/[budgetId]/index.tsx
index 4097c211..7b38aacf 100644
--- a/apps/mobile/app/(app)/budget/[budgetId]/index.tsx
+++ b/apps/mobile/app/(app)/budget/[budgetId]/index.tsx
@@ -251,11 +251,12 @@ export default function BudgetDetailScreen() {
)}
renderSectionHeader={({ section: { title, sum } }) => (
-
- {title}
+
+ {title}
diff --git a/apps/mobile/app/(app)/transaction/new-record.tsx b/apps/mobile/app/(app)/transaction/new-record.tsx
index dea3e3a4..f28a710e 100644
--- a/apps/mobile/app/(app)/transaction/new-record.tsx
+++ b/apps/mobile/app/(app)/transaction/new-record.tsx
@@ -20,7 +20,12 @@ import { useLingui } from '@lingui/react'
import { createId } from '@paralleldrive/cuid2'
import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal'
import * as Haptics from 'expo-haptics'
-import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router'
+import {
+ Link,
+ useLocalSearchParams,
+ useNavigation,
+ useRouter,
+} from 'expo-router'
import { CameraIcon, KeyboardIcon, Trash2Icon } from 'lucide-react-native'
import { useEffect, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -28,10 +33,12 @@ import {
ActivityIndicator,
Alert,
Dimensions,
+ Image,
Keyboard,
ScrollView,
View,
} from 'react-native'
+import { z } from 'zod'
const DEFAULT_CATEGORY_CONFIG = {
type: 'EXPENSE',
@@ -54,7 +61,13 @@ export default function NewRecordScreen() {
const { expenseCategories } = useCategoryList()
const params = useLocalSearchParams()
- const parsedParams = zUpdateTransaction.parse(params)
+ const { blobObjectUrl, blobObjectId, ...parsedParams } = zUpdateTransaction
+ .extend({
+ blobObjectId: z.string().nullable().optional(),
+ blobObjectUrl: z.string().nullable().optional(),
+ })
+ .parse(params)
+
const defaultValues: TransactionFormValues = {
date: new Date(),
amount: 0,
@@ -80,30 +93,45 @@ export default function NewRecordScreen() {
// biome-ignore lint/correctness/useExhaustiveDependencies:
useEffect(() => {
navigation.setOptions({
- headerTitle: () => (
- {
- setPage(Number(value))
- Keyboard.dismiss()
- ref.current?.scrollTo({
- y: 0,
- x: value === '0' ? 0 : width,
- animated: true,
- })
- }}
- className="w-[160px]"
- >
-
-
-
-
-
-
-
-
-
- ),
+ headerTitle: () =>
+ blobObjectUrl ? (
+
+
+
+ ) : (
+ {
+ setPage(Number(value))
+ Keyboard.dismiss()
+ ref.current?.scrollTo({
+ y: 0,
+ x: value === '0' ? 0 : width,
+ animated: true,
+ })
+ }}
+ className="w-[160px]"
+ >
+
+
+
+
+
+
+
+
+
+ ),
headerRight: () =>
parsedParams?.id ? (
) : null,
})
- }, [page])
+ }, [page, blobObjectUrl])
const handleCreateTransaction = async (values: TransactionFormValues) => {
try {
@@ -134,6 +162,7 @@ export default function NewRecordScreen() {
data: {
...values,
amount: values.categoryId ? values.amount : -Math.abs(values.amount),
+ blobAttachmentIds: blobObjectId ? [blobObjectId] : undefined,
},
})
} catch (error) {
diff --git a/apps/mobile/components/budget/budget-item.tsx b/apps/mobile/components/budget/budget-item.tsx
index 8cabab67..36d8b72f 100644
--- a/apps/mobile/components/budget/budget-item.tsx
+++ b/apps/mobile/components/budget/budget-item.tsx
@@ -51,10 +51,10 @@ export const BudgetItem: FC = ({ budget }) => {
>
-
+
{budget.name}
diff --git a/apps/mobile/components/common/bottom-sheet.tsx b/apps/mobile/components/common/bottom-sheet.tsx
index 2c575906..092153c3 100644
--- a/apps/mobile/components/common/bottom-sheet.tsx
+++ b/apps/mobile/components/common/bottom-sheet.tsx
@@ -2,11 +2,13 @@ import { useColorPalette } from '@/hooks/use-color-palette'
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
+ type BottomSheetBackgroundProps,
BottomSheetModal,
type BottomSheetModalProps,
} from '@gorhom/bottom-sheet'
import type { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'
import { forwardRef, useCallback } from 'react'
+import { View } from 'react-native'
import { FullWindowOverlay } from 'react-native-screens'
export const BottomSheet = forwardRef<
@@ -34,12 +36,20 @@ export const BottomSheet = forwardRef<
[],
)
+ const backgroundComponent = useCallback(
+ (props: BottomSheetBackgroundProps) => (
+
+ ),
+ [],
+ )
+
return (
diff --git a/apps/mobile/components/form-fields/currency-field.tsx b/apps/mobile/components/form-fields/currency-field.tsx
index a1302c28..e753854d 100644
--- a/apps/mobile/components/form-fields/currency-field.tsx
+++ b/apps/mobile/components/form-fields/currency-field.tsx
@@ -30,7 +30,7 @@ export function CurrencyField({
className,
)}
>
- {value}
+ {value}
)}
- {item.name}
- {item.percentage}%
+ {item.name}
+ {item.percentage}%
)
}}
diff --git a/apps/mobile/components/home/header.tsx b/apps/mobile/components/home/header.tsx
index 6d20517e..8ffa8d0e 100644
--- a/apps/mobile/components/home/header.tsx
+++ b/apps/mobile/components/home/header.tsx
@@ -1,7 +1,8 @@
import { useUser } from '@clerk/clerk-expo'
import { useRouter } from 'expo-router'
-import { Text, TouchableOpacity, View } from 'react-native'
+import { TouchableOpacity, View } from 'react-native'
import { UserAvatar } from '../common/user-avatar'
+import { Text } from '../ui/text'
import { type HomeFilter, SelectFilter } from './select-filter'
import { SelectWalletAccount } from './select-wallet-account'
@@ -31,7 +32,7 @@ export function HomeHeader({
-
+
{user?.fullName ?? user?.primaryEmailAddress?.emailAddress}
{value}
diff --git a/apps/mobile/components/home/wallet-statistics.tsx b/apps/mobile/components/home/wallet-statistics.tsx
index 03373629..3c6db1e0 100644
--- a/apps/mobile/components/home/wallet-statistics.tsx
+++ b/apps/mobile/components/home/wallet-statistics.tsx
@@ -128,7 +128,7 @@ export function WalletStatistics({
className="!border-0 h-auto native:h-auto flex-col items-center gap-3"
>
-
+
{options.find((option) => option.value === view)?.label}
diff --git a/apps/mobile/components/transaction/draft-transaction-item.tsx b/apps/mobile/components/transaction/draft-transaction-item.tsx
index 2a5962a4..d6f45c2f 100644
--- a/apps/mobile/components/transaction/draft-transaction-item.tsx
+++ b/apps/mobile/components/transaction/draft-transaction-item.tsx
@@ -6,7 +6,7 @@ import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMutation, useMutationState } from '@tanstack/react-query'
import { Link } from 'expo-router'
-import { CircleAlert } from 'lucide-react-native'
+import { CircleAlert, SparklesIcon } from 'lucide-react-native'
import type { FC } from 'react'
import { ActivityIndicator, Alert, Image, Pressable, View } from 'react-native'
import { AmountFormat } from '../common/amount-format'
@@ -89,7 +89,7 @@ export const DraftTransactionItem: FC = ({
if (latestState?.state?.status === 'pending') {
return (
{
Alert.alert('', t(i18n)`Transaction is processing...`, [
{
@@ -106,16 +106,28 @@ export const DraftTransactionItem: FC = ({
])
}}
>
-
-
+
+
+
+
- {t(i18n)`Processing...`}
+ {t(i18n)`AI Processing...`}
-
+ e.stopPropagation()}
+ >
+
+
)
}
@@ -124,19 +136,31 @@ export const DraftTransactionItem: FC = ({
return (
-
-
+
+
+
+
{latestState?.state?.failureReason?.message ??
t(i18n)`Cannot process image`}
-
+ e.stopPropagation()}
+ >
+
+
)
}
@@ -147,26 +171,86 @@ export const DraftTransactionItem: FC = ({
push
href={{
pathname: '/transaction/new-record',
- // biome-ignore lint/suspicious/noExplicitAny:
- params: transaction as any,
+ params: {
+ ...transaction,
+ blobObjectUrl: transaction.blobObject?.url,
+ blobObjectId: transaction.blobObject?.id,
+ // biome-ignore lint/suspicious/noExplicitAny:
+ } as any,
}}
>
-
-
+
+
name={iconName as any}
- className="size-5 text-foreground"
+ className="size-6 text-secondary-foreground"
/>
-
- {transactionName}
-
+ {transaction.blobObject && (
+
+
+
+ )}
+
+
+
+
+ {transactionName}
+
+
+
+
+
+
+
+
+ {t(i18n)`Processed by AI`}
+
+
+ {/* {transaction.walletAccount && (
+
+
+ name={transaction.walletAccount.icon as any}
+ className="size-4 text-muted-foreground"
+ />
+
+ {transaction.walletAccount.name}
+
+
+ )}
+ {transaction.budget && (
+
+
+
+ {transaction.budget.name}
+
+
+ )} */}
+
-
)
diff --git a/apps/mobile/components/transaction/draft-transaction-list.tsx b/apps/mobile/components/transaction/draft-transaction-list.tsx
index 2b85e5a4..cbe44f3f 100644
--- a/apps/mobile/components/transaction/draft-transaction-list.tsx
+++ b/apps/mobile/components/transaction/draft-transaction-list.tsx
@@ -17,9 +17,7 @@ export function DraftTransactionList() {
return (
- {t(
- i18n,
- )`Waiting for review`}
+ {t(i18n)`Waiting for review`}
{draftTransactions.length}
diff --git a/apps/mobile/components/transaction/scanner.tsx b/apps/mobile/components/transaction/scanner.tsx
index 896303f8..52e4b138 100644
--- a/apps/mobile/components/transaction/scanner.tsx
+++ b/apps/mobile/components/transaction/scanner.tsx
@@ -60,8 +60,10 @@ export function Scanner({
useTransactionStore()
const { entitlement } = useUserEntitlements()
- const todayTransactions = transactions.filter((t) =>
- dayjsExtended(t.createdAt).isSame(dayjsExtended(), 'day'),
+ const todayTransactions = transactions.filter(
+ (t) =>
+ !!t.blobAttachments?.length && // TODO: Better way to check if transaction is from AI
+ dayjsExtended(t.createdAt).isSame(dayjsExtended(), 'day'),
)
const { mutateAsync } = useMutation({
@@ -95,7 +97,7 @@ export function Scanner({
todayTransactions.length
async function processImages(uris: string[]) {
- if (transactionQuota - uris.length <= 0) {
+ if (transactionQuota - uris.length < 0) {
onLimitExceeded?.()
return
}
diff --git a/apps/mobile/components/transaction/select-account-field.tsx b/apps/mobile/components/transaction/select-account-field.tsx
index da1cd373..3975c62e 100644
--- a/apps/mobile/components/transaction/select-account-field.tsx
+++ b/apps/mobile/components/transaction/select-account-field.tsx
@@ -14,6 +14,7 @@ import { Keyboard, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { BottomSheet } from '../common/bottom-sheet'
import GenericIcon from '../common/generic-icon'
+import { Badge } from '../ui/badge'
import { Button } from '../ui/button'
import { Text } from '../ui/text'
@@ -68,13 +69,15 @@ export function SelectAccountField({
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
ListHeaderComponent={
-
- {t(i18n)`Wallet Accounts`}
-
+
+ {t(
+ i18n,
+ )`Wallet Accounts`}
+
}
contentContainerStyle={{ paddingBottom: bottom + 16 }}
renderItem={({ item }) => (
-
+
-
+
{item.name}
diff --git a/apps/mobile/components/transaction/select-category-field.tsx b/apps/mobile/components/transaction/select-category-field.tsx
index 47d309ae..d82982be 100644
--- a/apps/mobile/components/transaction/select-category-field.tsx
+++ b/apps/mobile/components/transaction/select-category-field.tsx
@@ -141,7 +141,7 @@ export function SelectCategoryField({
bounce
loop
animationType="bounce"
- className="!text-base line-clamp-1 text-center text-muted-foreground"
+ className="line-clamp-1 text-center font-regular text-muted-foreground"
>
{item.name}
diff --git a/apps/mobile/components/transaction/transaction-form.tsx b/apps/mobile/components/transaction/transaction-form.tsx
index 8b468ce8..faf57e00 100644
--- a/apps/mobile/components/transaction/transaction-form.tsx
+++ b/apps/mobile/components/transaction/transaction-form.tsx
@@ -1,11 +1,11 @@
import { useUserEntitlements } from '@/hooks/use-purchases'
import { sleep } from '@/lib/utils'
-import type { TransactionFormValues } from '@6pm/validation'
+import type { BlobObject, TransactionFormValues } from '@6pm/validation'
import type { BottomSheetModal } from '@gorhom/bottom-sheet'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import * as Haptics from 'expo-haptics'
-import { useRouter } from 'expo-router'
+import { Link, useRouter } from 'expo-router'
import { Trash2Icon } from 'lucide-react-native'
import { useRef } from 'react'
import {
@@ -16,6 +16,7 @@ import {
useWatch,
} from 'react-hook-form'
import { ScrollView, View } from 'react-native'
+import { Image } from 'react-native'
import Animated, {
useAnimatedKeyboard,
useAnimatedStyle,
@@ -40,6 +41,7 @@ type TransactionFormProps = {
onOpenScanner?: () => void
form: UseFormReturn
sideOffset?: number
+ blobAttachments?: BlobObject[] | null
}
export function TransactionAmount() {
@@ -109,6 +111,7 @@ export const TransactionForm = ({
onDelete,
// onOpenScanner,
sideOffset,
+ blobAttachments,
}: TransactionFormProps) => {
const { i18n } = useLingui()
@@ -132,18 +135,37 @@ export const TransactionForm = ({
{/* */}
- (
-
+
+ (
+
+ )}
+ />
+ {!!blobAttachments?.length && (
+
+
+
)}
- />
+
{onDelete ? (