Skip to content

Commit

Permalink
feat(mobile): add transaction queue and allow select multiple images (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 authored Sep 2, 2024
1 parent 6d3d604 commit 7dfac0b
Show file tree
Hide file tree
Showing 16 changed files with 539 additions and 120 deletions.
26 changes: 15 additions & 11 deletions apps/mobile/app/(app)/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HomeHeader } from '@/components/home/header'
import { HomeFilter } from '@/components/home/select-filter'
import { TimeRangeControl } from '@/components/home/time-range-control'
import { HomeView, WalletStatistics } from '@/components/home/wallet-statistics'
import { DraftTransactionList } from '@/components/transaction/draft-transaction-list'
import { HandyArrow } from '@/components/transaction/handy-arrow'
import { TransactionItem } from '@/components/transaction/transaction-item'
import { Text } from '@/components/ui/text'
Expand Down Expand Up @@ -108,17 +109,20 @@ export default function HomeScreen() {
<SectionList
ListHeaderComponent={
filter === HomeFilter.All ? (
<View className="p-6 pb-4">
<WalletStatistics
view={view}
onViewChange={(selected) => {
setView(selected)
setCategoryId(undefined)
}}
walletAccountId={walletAccountId}
categoryId={categoryId}
onCategoryChange={setCategoryId}
/>
<View>
<View className="p-6 pb-4">
<WalletStatistics
view={view}
onViewChange={(selected) => {
setView(selected)
setCategoryId(undefined)
}}
walletAccountId={walletAccountId}
categoryId={categoryId}
onCategoryChange={setCategoryId}
/>
</View>
<DraftTransactionList />
</View>
) : null
}
Expand Down
42 changes: 28 additions & 14 deletions apps/mobile/app/(app)/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useSeed } from '@/hooks/use-seed'
import { useColorScheme } from '@/hooks/useColorScheme'
import { theme } from '@/lib/theme'
import { useLocale } from '@/locales/provider'
import { useTransactionStore } from '@/stores/transaction/store'
import { useUserSettingsStore } from '@/stores/user-settings/store'
import { useAuth } from '@clerk/clerk-expo'
import { t } from '@lingui/macro'
Expand All @@ -38,6 +39,7 @@ import {
ScrollTextIcon,
ShapesIcon,
Share2Icon,
SparklesIcon,
StarIcon,
SwatchBookIcon,
WalletCardsIcon,
Expand All @@ -59,6 +61,7 @@ export default function SettingsScreen() {
const { setEnabledPushNotifications, enabledPushNotifications } =
useUserSettingsStore()
const { startSeed } = useSeed()
const { draftTransactions } = useTransactionStore()

async function handleCopyVersion() {
const fullVersion = `${Application.nativeApplicationVersion} - ${Updates.updateId ?? 'Embedded'}`
Expand Down Expand Up @@ -108,17 +111,29 @@ export default function SettingsScreen() {
}
/>
</Link>
<Link href="/magic-inbox" asChild disabled>
<Link href="/review-transactions" asChild>
<MenuItem
label={t(i18n)`Magic inbox`}
icon={InboxIcon}
label={t(i18n)`Review transactions`}
icon={SparklesIcon}
rightSection={
<Badge variant="outline">
<Text className="text-xs">{t(i18n)`Coming soon`}</Text>
<Badge
variant={draftTransactions.length ? 'default' : 'outline'}
>
<Text className="text-xs">{draftTransactions.length}</Text>
</Badge>
}
/>
</Link>
<MenuItem
label={t(i18n)`Magic inbox`}
icon={InboxIcon}
rightSection={
<Badge variant="outline">
<Text className="text-xs">{t(i18n)`Coming soon`}</Text>
</Badge>
}
disabled
/>
</View>
</View>
<View className="gap-2">
Expand Down Expand Up @@ -207,15 +222,14 @@ export default function SettingsScreen() {
}
/>
</Link>
<Link href="/feedback" asChild disabled>
<MenuItem
label={t(i18n)`Send feedback`}
icon={MessageSquareQuoteIcon}
rightSection={
<ChevronRightIcon className="h-5 w-5 text-primary" />
}
/>
</Link>
<MenuItem
label={t(i18n)`Send feedback`}
icon={MessageSquareQuoteIcon}
rightSection={
<ChevronRightIcon className="h-5 w-5 text-primary" />
}
disabled
/>
<MenuItem
label={t(i18n)`Rate 6pm on App Store`}
icon={StarIcon}
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ export default function AuthenticatedLayout() {
name="transaction/new-record"
options={{
presentation: 'modal',
headerShown: false,
headerTitle: '',
// headerShown: false,
}}
/>
<Stack.Screen
name="review-transactions"
options={{
headerTitle: t(i18n)`Review transactions`,
}}
/>
<Stack.Screen
Expand Down
27 changes: 27 additions & 0 deletions apps/mobile/app/(app)/review-transactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DraftTransactionItem } from '@/components/transaction/draft-transaction-item'
import { Text } from '@/components/ui/text'
import { useTransactionStore } from '@/stores/transaction/store'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { FlatList } from 'react-native'

export default function ReviewTransactionsScreen() {
const { draftTransactions } = useTransactionStore()
const { i18n } = useLingui()

return (
<FlatList
className="bg-card"
data={draftTransactions}
renderItem={({ item }) => (
<DraftTransactionItem transactionId={item.id} />
)}
keyExtractor={(item) => item.id}
ListEmptyComponent={
<Text className="m-6 mb-9 text-center font-sans text-muted-foreground">
{t(i18n)`Your pending AI transactions will show up here`}
</Text>
}
/>
)
}
140 changes: 100 additions & 40 deletions apps/mobile/app/(app)/transaction/new-record.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { toast } from '@/components/common/toast'
import { Scanner } from '@/components/transaction/scanner'
import { TransactionForm } from '@/components/transaction/transaction-form'
import { Button } from '@/components/ui/button'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useUserMetadata } from '@/hooks/use-user-metadata'
import { useWallets, walletQueries } from '@/queries/wallet'
import { useCreateTransaction } from '@/stores/transaction/hooks'
import { useTransactionStore } from '@/stores/transaction/store'
import { useDefaultCurrency } from '@/stores/user-settings/hooks'
import {
type TransactionFormValues,
Expand All @@ -17,15 +20,22 @@ import { createId } from '@paralleldrive/cuid2'
import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal'
import { useQueryClient } from '@tanstack/react-query'
import * as Haptics from 'expo-haptics'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useRef, useState } from 'react'
import { 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'
import { ActivityIndicator, Alert, View } from 'react-native'
import PagerView from 'react-native-pager-view'
import {
ActivityIndicator,
Alert,
Keyboard,
ScrollView,
View,
useWindowDimensions,
} from 'react-native'

export default function NewRecordScreen() {
const { i18n } = useLingui()
const ref = useRef<PagerView>(null)
const ref = useRef<ScrollView>(null)
const queryClient = useQueryClient()
const router = useRouter()
const { data: walletAccounts } = useWallets()
Expand All @@ -34,6 +44,9 @@ export default function NewRecordScreen() {
const { sideOffset, ...rootProps } = useModalPortalRoot()
const [page, setPage] = useState<number>(0)
const { defaultBudgetId } = useUserMetadata()
const navigation = useNavigation()
const { width } = useWindowDimensions()
const { removeDraftTransaction } = useTransactionStore()

const params = useLocalSearchParams()
const parsedParams = zUpdateTransaction.parse(params)
Expand All @@ -54,13 +67,59 @@ export default function NewRecordScreen() {

const { mutateAsync } = useCreateTransaction()

// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
navigation.setOptions({
headerTitle: () => (
<Tabs
value={page.toString()}
onValueChange={(value) => {
setPage(Number(value))
Keyboard.dismiss()
ref.current?.scrollTo({
y: 0,
x: value === '0' ? 0 : width,
animated: true,
})
}}
className="w-[150px]"
>
<TabsList>
<TabsTrigger value="0">
<KeyboardIcon className="!text-primary size-5" />
</TabsTrigger>
<TabsTrigger value="1">
<CameraIcon className="!text-primary size-5" />
</TabsTrigger>
</TabsList>
</Tabs>
),
headerRight: () =>
parsedParams?.id ? (
<Button
size="icon"
variant="ghost"
onPress={() => {
removeDraftTransaction(parsedParams.id!)
router.back()
}}
>
<Trash2Icon className="size-6 text-destructive" />
</Button>
) : null,
})
}, [page])

const handleCreateTransaction = async (values: TransactionFormValues) => {
try {
if (parsedParams?.id) {
removeDraftTransaction(parsedParams.id)
}
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
router.back()
toast.success(t(i18n)`Transaction created`)

await mutateAsync({ id: createId(), data: values })
await mutateAsync({ id: parsedParams.id || createId(), data: values })

// TODO: remove this after the wallet store is implemented
queryClient.invalidateQueries({
Expand All @@ -84,42 +143,43 @@ export default function NewRecordScreen() {

return (
<View className="flex-1 bg-card" {...rootProps}>
<PagerView
<ScrollView
ref={ref}
overdrag={false}
orientation="vertical"
initialPage={0}
style={{ flex: 1 }}
onPageSelected={({ nativeEvent }) => setPage(nativeEvent.position)}
offscreenPageLimit={2}
horizontal
pagingEnabled
bounces={false}
scrollEventThrottle={16}
onMomentumScrollEnd={({ nativeEvent }) => {
const page = Math.round(nativeEvent.contentOffset.x / width)
setPage(page)
Keyboard.dismiss()
}}
showsHorizontalScrollIndicator={false}
>
<TransactionForm
sideOffset={sideOffset}
form={transactionForm}
onSubmit={handleCreateTransaction}
onCancel={router.back}
onOpenScanner={() => {
ref.current?.setPage(1)
}}
/>
<Scanner
onScanStart={() => ref.current?.setScrollEnabled(false)}
onScanResult={(result) => {
transactionForm.reset(
{
...defaultValues,
...result,
},
{
keepDefaultValues: false,
},
)
ref.current?.setScrollEnabled(true)
ref.current?.setPage(0)
}}
shouldRender={page === 1}
/>
</PagerView>
<View style={{ width }}>
<TransactionForm
sideOffset={sideOffset}
form={transactionForm}
onSubmit={handleCreateTransaction}
onCancel={router.back}
onOpenScanner={() => {
ref.current?.scrollTo({
y: 0,
x: width,
animated: true,
})
}}
/>
</View>
<View style={{ width }}>
<Scanner
onScanStart={() => {
toast.success(t(i18n)`Transaction added to processing queue`)
}}
shouldRender={page === 1}
/>
</View>
</ScrollView>
<PortalHost name="transaction-form" />
</View>
)
Expand Down
Loading

0 comments on commit 7dfac0b

Please sign in to comment.