Skip to content

Commit

Permalink
feat(mobile): [Scanner] nest scanner inside new transaction screen
Browse files Browse the repository at this point in the history
  • Loading branch information
Quốc Khánh authored and bkdev98 committed Jul 17, 2024
1 parent 0ce0a73 commit af630eb
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 90 deletions.
23 changes: 16 additions & 7 deletions apps/mobile/app/(app)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Button } from '@/components/ui/button'
import { useColorScheme } from '@/hooks/useColorScheme'
import { theme } from '@/lib/theme'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Tabs } from 'expo-router'
import { Link, Tabs } from 'expo-router'
import {
// BarChartBigIcon,
CogIcon,
LandPlotIcon,
ScanTextIcon,
PlusIcon,
WalletIcon,
} from 'lucide-react-native'

Expand Down Expand Up @@ -47,16 +49,23 @@ export default function TabLayout() {
headerTitle: t(i18n)`Budgets`,
tabBarShowLabel: false,
tabBarIcon: ({ color }) => <LandPlotIcon color={color} />,
headerRight: () => (
<Link href="/budget/new-budget" asChild>
<Button size="icon" variant="ghost" className="mr-4">
<PlusIcon className="size-6 text-primary" />
</Button>
</Link>
),
}}
/>
<Tabs.Screen
name="scanner"
{/* <Tabs.Screen
name="reports"
options={{
headerTitle: t(i18n)`Scanner`,
headerTitle: t(i18n)`Reports`,
tabBarShowLabel: false,
tabBarIcon: ({ color }) => <ScanTextIcon color={color} />,
tabBarIcon: ({ color }) => <BarChartBigIcon color={color} />,
}}
/>
/> */}
<Tabs.Screen
name="settings"
options={{
Expand Down
30 changes: 21 additions & 9 deletions apps/mobile/app/(app)/transaction/[transactionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { TransactionForm } from '@/components/transaction/transaction-form'
import { deleteTransaction, updateTransaction } from '@/mutations/transaction'
import { transactionQueries, useTransactionDetail } from '@/queries/transaction'
import { walletQueries } from '@/queries/wallet'
import {
type TransactionFormValues,
zTransactionFormValues,
} from '@6pm/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as Haptics from 'expo-haptics'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { LoaderIcon } from 'lucide-react-native'
import { useForm } from 'react-hook-form'
import { Alert, View } from 'react-native'

export default function EditRecordScreen() {
Expand All @@ -16,6 +22,20 @@ export default function EditRecordScreen() {
const { data: transaction } = useTransactionDetail(transactionId!)
const router = useRouter()
const queryClient = useQueryClient()

const transactionForm = useForm<TransactionFormValues>({
resolver: zodResolver(zTransactionFormValues),
defaultValues: {
walletAccountId: transaction?.walletAccountId,
currency: transaction?.currency,
amount: Math.abs(transaction?.amount ?? 0),
date: transaction?.date,
note: transaction?.note ?? '',
budgetId: transaction?.budgetId ?? undefined,
categoryId: transaction?.categoryId ?? undefined,
},
})

const { mutateAsync } = useMutation({
mutationFn: updateTransaction,
onError(error) {
Expand Down Expand Up @@ -89,17 +109,9 @@ export default function EditRecordScreen() {

return (
<TransactionForm
form={transactionForm}
onSubmit={(values) => mutateAsync({ id: transaction.id, data: values })}
onCancel={router.back}
defaultValues={{
walletAccountId: transaction.walletAccountId,
currency: transaction.currency,
amount: Math.abs(transaction.amount),
date: transaction.date,
note: transaction.note ?? '',
budgetId: transaction.budgetId ?? undefined,
categoryId: transaction.categoryId ?? undefined,
}}
onDelete={handleDelete}
/>
)
Expand Down
81 changes: 65 additions & 16 deletions apps/mobile/app/(app)/transaction/new-record.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
import { toast } from '@/components/common/toast'
import { Scanner } from '@/components/transaction/scanner'
import { TransactionForm } from '@/components/transaction/transaction-form'
import { createTransaction } from '@/mutations/transaction'
import { transactionQueries } from '@/queries/transaction'
import { useWallets, walletQueries } from '@/queries/wallet'
import { zUpdateTransaction } from '@6pm/validation'
import {
type TransactionFormValues,
zTransactionFormValues,
zUpdateTransaction,
} from '@6pm/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as Haptics from 'expo-haptics'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { LoaderIcon } from 'lucide-react-native'
import { useRef } from 'react'
import { useForm } from 'react-hook-form'
import { Alert, View } from 'react-native'
import PagerView from 'react-native-pager-view'

export default function NewRecordScreen() {
const router = useRouter()
const params = useLocalSearchParams()
const defaultValues = zUpdateTransaction.parse(params)
const { data: walletAccounts } = useWallets()
const { i18n } = useLingui()
const ref = useRef<PagerView>(null)
const queryClient = useQueryClient()
const router = useRouter()
const { data: walletAccounts } = useWallets()
const defaultWallet = walletAccounts?.[0]

const params = useLocalSearchParams()
const parsedParams = zUpdateTransaction.parse(params)
const defaultValues = {
date: new Date(),
amount: 0,
currency: 'USD',
note: '',
walletAccountId: defaultWallet?.id,
...parsedParams,
}

const transactionForm = useForm<TransactionFormValues>({
resolver: zodResolver(zTransactionFormValues),
defaultValues,
})

const { mutateAsync } = useMutation({
mutationFn: createTransaction,
onError(error) {
Expand All @@ -42,8 +68,6 @@ export default function NewRecordScreen() {
},
})

const defaultWallet = walletAccounts?.[0]

if (!defaultWallet) {
return (
<View className="flex-1 items-center bg-muted justify-center">
Expand All @@ -53,14 +77,39 @@ export default function NewRecordScreen() {
}

return (
<TransactionForm
onSubmit={mutateAsync}
onCancel={router.back}
defaultValues={{
walletAccountId: defaultWallet.id,
currency: defaultWallet.preferredCurrency ?? 'USD',
...defaultValues,
}}
/>
<View className="flex-1 bg-card">
<PagerView
ref={ref}
overdrag={false}
orientation="vertical"
initialPage={0}
style={{ flex: 1 }}
>
<TransactionForm
form={transactionForm}
onSubmit={mutateAsync}
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)
}}
/>
</PagerView>
</View>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { ScanningOverlay } from '@/components/scanner/scanning-overlay'
import { Button } from '@/components/ui/button'
import { Text } from '@/components/ui/text'
import { getAITransactionData } from '@/mutations/transaction'
import type { UpdateTransaction } from '@6pm/validation'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMutation } from '@tanstack/react-query'
import { type CameraType, CameraView, useCameraPermissions } from 'expo-camera'
import * as Haptics from 'expo-haptics'
import { SaveFormat, manipulateAsync } from 'expo-image-manipulator'
import * as ImagePicker from 'expo-image-picker'
import { useRouter } from 'expo-router'
import {
CameraIcon,
ImagesIcon,
Expand All @@ -22,35 +22,40 @@ import { cssInterop } from 'nativewind'
import { useRef, useState } from 'react'
import { Alert } from 'react-native'
import { ImageBackground, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

cssInterop(CameraView, {
className: {
target: 'style',
},
})

export default function ScannerScreen() {
type ScannerProps = {
onScanStart?: () => void
onScanResult: (result: UpdateTransaction) => void
}

export function Scanner({ onScanStart, onScanResult }: ScannerProps) {
const camera = useRef<CameraView>(null)
const router = useRouter()
const [facing, setFacing] = useState<CameraType>('back')
const [permission, requestPermission] = useCameraPermissions()
const [imageUri, setImageUri] = useState<string | null>(null)
const { i18n } = useLingui()
const { bottom } = useSafeAreaInsets()

const { mutateAsync } = useMutation({
mutationFn: getAITransactionData,
onMutate() {
onScanStart?.()
},
onError(error) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
Alert.alert(error.message ?? t(i18n)`Cannot extract transaction data`)
setImageUri(null)
},
onSuccess(result) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
router.push({
pathname: '/transaction/new-record',
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
params: result as any,
})
onScanResult(result)
setImageUri(null)
},
})
Expand Down Expand Up @@ -131,6 +136,7 @@ export default function ScannerScreen() {
<ImageBackground
source={{ uri: imageUri }}
className="flex-1 items-center"
style={{ paddingBottom: bottom }}
>
<ScanningOverlay />
{/* <ScanningIndicator /> */}
Expand All @@ -140,7 +146,7 @@ export default function ScannerScreen() {
<Button
variant="secondary"
size="icon"
className="w-auto !opacity-100 h-auto p-1 absolute bottom-6 rounded-full bg-primary-foreground"
className="w-auto !opacity-100 h-auto p-1 absolute bottom-8 rounded-full bg-primary-foreground"
disabled
>
<AnimatedRing />
Expand All @@ -152,11 +158,16 @@ export default function ScannerScreen() {

return (
<View className="flex-1 bg-card">
<CameraView ref={camera} className="flex-1 items-center" facing={facing}>
<CameraView
ref={camera}
className="flex-1 items-center"
facing={facing}
style={{ paddingBottom: bottom }}
>
<View className="top-6 bg-background/50 p-2 px-4 rounded-md">
<Text>{t(i18n)`Take a picture of your transaction`}</Text>
</View>
<View className="absolute bottom-6 left-6 right-6 flex-row items-center justify-between gap-4">
<View className="absolute bottom-8 left-6 right-6 flex-row items-center justify-between gap-4">
<Button
variant="secondary"
size="icon"
Expand Down
Loading

0 comments on commit af630eb

Please sign in to comment.