Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): setup persistor for tanstack query #127

Merged
merged 3 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 38 additions & 30 deletions apps/api/v1/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ import {
} from '../services/transaction.service'
import { canUserReadWallet, findUserWallet } from '../services/wallet.service'

const zTransactionParamValidator = zValidator(
'param',
z.object({
transactionId: z.string(),
}),
)

const router = new Hono()

.get(
Expand Down Expand Up @@ -157,14 +164,24 @@ const router = new Hono()
return c.json(transaction, 201)
})

.get('/:transactionId', zTransactionParamValidator, async (c) => {
const user = getAuthUserStrict(c)
const { transactionId } = c.req.valid('param')

const transaction = await findTransaction({ transactionId })

if (
!(transaction && (await canUserReadTransaction({ user, transaction })))
) {
return c.json({ message: 'transaction not found' }, 404)
}

return c.json(transaction)
})

.put(
'/:transactionId',
zValidator(
'param',
z.object({
transactionId: z.string(),
}),
),
zTransactionParamValidator,
zValidator('json', zUpdateTransaction),
async (c) => {
const { transactionId } = c.req.valid('param')
Expand Down Expand Up @@ -236,35 +253,26 @@ const router = new Hono()
},
)

.delete(
'/:transactionId',
zValidator(
'param',
z.object({
transactionId: z.string(),
}),
),
async (c) => {
const { transactionId } = c.req.valid('param')
const user = getAuthUserStrict(c)
.delete('/:transactionId', zTransactionParamValidator, async (c) => {
const { transactionId } = c.req.valid('param')
const user = getAuthUserStrict(c)

const transaction = await findTransaction({ transactionId })
const transaction = await findTransaction({ transactionId })

if (
!(transaction && (await canUserReadTransaction({ user, transaction })))
) {
return c.json({ message: 'transaction not found' }, 404)
}
if (
!(transaction && (await canUserReadTransaction({ user, transaction })))
) {
return c.json({ message: 'transaction not found' }, 404)
}

if (!(await canUserDeleteTransaction({ user, transaction }))) {
return c.json({ message: 'user cannot delete transaction' }, 403)
}
if (!(await canUserDeleteTransaction({ user, transaction }))) {
return c.json({ message: 'user cannot delete transaction' }, 403)
}

await deleteTransaction({ transactionId })
await deleteTransaction({ transactionId })

return c.json(transaction)
},
)
return c.json(transaction)
})

.post('/ai', async (c) => {
const body = await c.req.parseBody()
Expand Down
9 changes: 9 additions & 0 deletions apps/api/v1/services/transaction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export async function findTransaction({
where: {
id: transactionId,
},
include: {
category: true,
},
})
}

Expand All @@ -117,6 +120,9 @@ export async function createTransaction({
...data,
createdByUserId: user.id,
},
include: {
category: true,
},
})

return transaction
Expand All @@ -134,6 +140,9 @@ export async function updateTransaction({
id: transactionId,
},
data,
include: {
category: true,
},
})

return transaction
Expand Down
7 changes: 7 additions & 0 deletions apps/mobile/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export default function AuthenticatedLayout() {
headerShown: false,
}}
/>
<Stack.Screen
name="transaction/[transactionId]"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="language"
options={{
Expand Down
106 changes: 106 additions & 0 deletions apps/mobile/app/(app)/transaction/[transactionId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 { 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 { Alert, View } from 'react-native'

export default function EditRecordScreen() {
const { i18n } = useLingui()
const { transactionId } = useLocalSearchParams<{ transactionId: string }>()
const { data: transaction } = useTransactionDetail(transactionId!)
const router = useRouter()
const queryClient = useQueryClient()
const { mutateAsync } = useMutation({
mutationFn: updateTransaction,
onError(error) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
Alert.alert(error.message)
},
onSuccess() {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
router.back()
},
async onSettled() {
await queryClient.invalidateQueries({
queryKey: transactionQueries.all,
})
await queryClient.invalidateQueries({
queryKey: walletQueries.list._def,
})
},
})
const { mutateAsync: mutateDelete } = useMutation({
mutationFn: deleteTransaction,
onMutate() {
router.back()
},
onError(error) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
Alert.alert(error.message)
},
onSuccess() {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
},
async onSettled() {
await queryClient.invalidateQueries({
queryKey: transactionQueries.all,
})
await queryClient.invalidateQueries({
queryKey: walletQueries.list._def,
})
},
throwOnError: true,
})

function handleDelete() {
Haptics.selectionAsync()
Alert.alert(
t(
i18n,
)`This will delete the transaction. Are you sure you want to continue?`,
'',
[
{
text: t(i18n)`Cancel`,
style: 'cancel',
},
{
text: t(i18n)`Delete`,
style: 'destructive',
onPress: () => mutateDelete(transactionId!),
},
],
)
}

if (!transaction) {
return (
<View className="flex-1 items-center bg-muted justify-center">
<LoaderIcon className="size-7 animate-spin text-primary" />
</View>
)
}

return (
<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}
/>
)
}
38 changes: 35 additions & 3 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,41 @@ import { useColorScheme } from '@/hooks/useColorScheme'
import { queryClient } from '@/lib/client'
import { LocaleProvider } from '@/locales/provider'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import AsyncStorage from '@react-native-async-storage/async-storage'
import NetInfo from '@react-native-community/netinfo'
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from '@react-navigation/native'
import { QueryClientProvider } from '@tanstack/react-query'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { focusManager, onlineManager } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { LinearGradient } from 'expo-linear-gradient'
import { cssInterop } from 'nativewind'
import { useEffect } from 'react'
import { AppState, type AppStateStatus, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Svg } from 'react-native-svg'

// Online status management
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected)
})
})

function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== 'web') {
focusManager.setFocused(status === 'active')
}
}

const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
})

cssInterop(Svg, {
className: {
target: 'style',
Expand Down Expand Up @@ -67,12 +90,21 @@ export default function RootLayout() {
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
})

useEffect(() => {
const subscription = AppState.addEventListener('change', onAppStateChange)

return () => subscription.remove()
}, [])

if (!fontsLoaded) {
return null
}

return (
<QueryClientProvider client={queryClient}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
>
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
Expand All @@ -99,6 +131,6 @@ export default function RootLayout() {
</ThemeProvider>
</LocaleProvider>
</ClerkProvider>
</QueryClientProvider>
</PersistQueryClientProvider>
)
}
23 changes: 18 additions & 5 deletions apps/mobile/components/transaction/transaction-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
import { zodResolver } from '@hookform/resolvers/zod'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { LandPlot, XIcon } from 'lucide-react-native'
import { LandPlot, Trash2Icon, XIcon } from 'lucide-react-native'
import { Controller, FormProvider, useForm } from 'react-hook-form'
import { ScrollView, View } from 'react-native'
import Animated, {
Expand All @@ -26,12 +26,14 @@ type TransactionFormProps = {
onSubmit: (data: TransactionFormValues) => void
defaultValues?: Partial<TransactionFormValues>
onCancel?: () => void
onDelete?: () => void
}

export const TransactionForm = ({
onSubmit,
defaultValues,
onCancel,
onDelete,
}: TransactionFormProps) => {
const { i18n } = useLingui()

Expand Down Expand Up @@ -65,9 +67,16 @@ export const TransactionForm = ({
>
<View className="flex-row justify-between items-center p-6 pb-0">
<SelectDateField />
<Button size="icon" variant="secondary" onPress={onCancel}>
<XIcon className="size-6 text-primary" />
</Button>
<View className="flex-row items-center gap-4">
{onDelete && (
<Button size="icon" variant="secondary" onPress={onDelete}>
<Trash2Icon className="size-6 text-primary" />
</Button>
)}
<Button size="icon" variant="secondary" onPress={onCancel}>
<XIcon className="size-6 text-primary" />
</Button>
</View>
</View>
<View className="flex-1 items-center justify-center pb-12">
<View className="w-full h-24 justify-end mb-4">
Expand Down Expand Up @@ -108,7 +117,11 @@ export const TransactionForm = ({
</View>
<SubmitButton
onPress={transactionForm.handleSubmit(onSubmit)}
disabled={transactionForm.formState.isLoading || !amount}
disabled={
transactionForm.formState.isLoading ||
!amount ||
!transactionForm.formState.isDirty
}
>
<Text>{t(i18n)`Save`}</Text>
</SubmitButton>
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ export const getHonoClient = async () => {
})
}

export const queryClient = new QueryClient()
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst',
gcTime: 1000 * 60 * 60 * 24 * 7, // 1 week
},
},
})
Loading