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..6acc44a6 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'
@@ -30,8 +31,8 @@ export function HomeHeader({
>
-
-
+
+
{user?.fullName ?? user?.primaryEmailAddress?.emailAddress}
{value !== HomeFilter.All && (
{value}
diff --git a/apps/mobile/components/home/select-wallet-account.tsx b/apps/mobile/components/home/select-wallet-account.tsx
index d73c3ed9..b03c1043 100644
--- a/apps/mobile/components/home/select-wallet-account.tsx
+++ b/apps/mobile/components/home/select-wallet-account.tsx
@@ -62,7 +62,7 @@ export function SelectWalletAccount({
className="!border-none !border-transparent !py-0 !h-6 flex-row items-center gap-2 self-start px-0"
>
{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/setting/profile-card.tsx b/apps/mobile/components/setting/profile-card.tsx
index 125bd398..4cab0ef3 100644
--- a/apps/mobile/components/setting/profile-card.tsx
+++ b/apps/mobile/components/setting/profile-card.tsx
@@ -166,8 +166,8 @@ export function ProfileCard() {
fallbackClassName="bg-background"
className="h-16 w-16"
/>
-
-
+
+
{user?.fullName ?? user?.primaryEmailAddress?.emailAddress}
{isPro && }
-
+
{isWealth
? t(i18n)`Wealth`
: isGrowth
diff --git a/apps/mobile/components/transaction/draft-transaction-item.tsx b/apps/mobile/components/transaction/draft-transaction-item.tsx
index 2a5962a4..4e30fedc 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..c7bebac8 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'
@@ -37,7 +38,7 @@ export function SelectAccountField({
<>
diff --git a/apps/mobile/components/transaction/select-budget-field.tsx b/apps/mobile/components/transaction/select-budget-field.tsx
index 30c124cb..e0d8c51a 100644
--- a/apps/mobile/components/transaction/select-budget-field.tsx
+++ b/apps/mobile/components/transaction/select-budget-field.tsx
@@ -106,7 +106,7 @@ export function SelectBudgetField({
{
Haptics.selectionAsync()
@@ -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 text-sm"
>
{item.name}
diff --git a/apps/mobile/components/transaction/transaction-form.tsx b/apps/mobile/components/transaction/transaction-form.tsx
index 8b468ce8..a7418528 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() {
@@ -96,6 +98,7 @@ function FormSubmitButton({
onPress={form.handleSubmit(onSubmit)}
onPressIn={Haptics.selectionAsync}
disabled={form.formState.isLoading || !amount}
+ className="flex-shrink-0"
>
{t(i18n)`Save`}
@@ -109,6 +112,7 @@ export const TransactionForm = ({
onDelete,
// onOpenScanner,
sideOffset,
+ blobAttachments,
}: TransactionFormProps) => {
const { i18n } = useLingui()
@@ -132,18 +136,37 @@ export const TransactionForm = ({
{/*
*/}
- (
-
+
+ (
+
+ )}
+ />
+ {!!blobAttachments?.length && (
+
+
+
)}
- />
+
{onDelete ? (
@@ -182,7 +205,7 @@ export const TransactionForm = ({
-
+
{
form.setValue('currency', walletAccount.preferredCurrency)
diff --git a/apps/mobile/components/transaction/transaction-item.tsx b/apps/mobile/components/transaction/transaction-item.tsx
index f45c3f59..f2207774 100644
--- a/apps/mobile/components/transaction/transaction-item.tsx
+++ b/apps/mobile/components/transaction/transaction-item.tsx
@@ -4,8 +4,9 @@ import type { TransactionPopulated } from '@6pm/validation'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Link } from 'expo-router'
+import { LandPlotIcon } from 'lucide-react-native'
import { type FC, useMemo } from 'react'
-import { Pressable, View } from 'react-native'
+import { Image, Pressable, View } from 'react-native'
import { AmountFormat } from '../common/amount-format'
import GenericIcon from '../common/generic-icon'
import { Text } from '../ui/text'
@@ -39,23 +40,72 @@ export const TransactionItem: FC = ({ transaction }) => {
params: { transactionId: transaction.id },
}}
>
-
-
+
+
name={iconName as any}
- className="size-5 text-foreground"
+ className="size-6 text-secondary-foreground"
/>
-
- {transactionName}
-
+ {!!transaction.blobAttachments?.length && (
+
+
+
+ )}
+
+
+
+
+ {transactionName}
+
+
+
+
+
+ {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/ui/button.tsx b/apps/mobile/components/ui/button.tsx
index 5638f9ee..c47be43b 100644
--- a/apps/mobile/components/ui/button.tsx
+++ b/apps/mobile/components/ui/button.tsx
@@ -35,7 +35,7 @@ const buttonVariants = cva(
)
const buttonTextVariants = cva(
- 'web:whitespace-nowrap font-medium native:text-base text-foreground text-sm web:transition-colors',
+ 'web:whitespace-nowrap font-semiBold native:text-base text-foreground text-sm web:transition-colors',
{
variants: {
variant: {
diff --git a/apps/mobile/components/ui/dropdown-menu.tsx b/apps/mobile/components/ui/dropdown-menu.tsx
index bc18846e..519eb615 100644
--- a/apps/mobile/components/ui/dropdown-menu.tsx
+++ b/apps/mobile/components/ui/dropdown-menu.tsx
@@ -47,7 +47,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
span]:line-clamp-1 native:h-11 web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 web:ring-offset-background',
- props.disabled && 'opacity-50 web:cursor-not-allowed',
+ 'flex h-10 native:h-11 flex-row items-center justify-between rounded-md border border-input bg-background px-3 py-2 font-regular text-base text-muted-foreground web:ring-offset-background web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 [&>span]:line-clamp-1',
+ props.disabled && 'web:cursor-not-allowed opacity-50',
className,
)}
{...props}
@@ -58,7 +58,7 @@ const SelectScrollUpButton = ({
return (
-
+
{extra}
))
diff --git a/apps/mobile/components/wallet/account-form.tsx b/apps/mobile/components/wallet/account-form.tsx
index ca7d59d8..474771dc 100644
--- a/apps/mobile/components/wallet/account-form.tsx
+++ b/apps/mobile/components/wallet/account-form.tsx
@@ -123,7 +123,7 @@ export const AccountForm = ({
i18n,
)`Current state`}