From 8ffbf185de379cd8ef81d70a2c34927c86bf57f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BB=91c=20Kh=C3=A1nh?= Date: Tue, 20 Aug 2024 22:23:23 +0700 Subject: [PATCH] feat(mobile): add onboard user budget screen (#230) --- apps/mobile/app/(app)/_layout.tsx | 8 +- apps/mobile/app/onboarding/_layout.tsx | 35 +++++ apps/mobile/app/onboarding/step-one.tsx | 40 ++++++ apps/mobile/app/onboarding/step-two.tsx | 133 ++++++++++++++++++ apps/mobile/components/budget/budget-item.tsx | 2 +- .../components/common/amount-format.tsx | 2 +- .../svg-assets/onboard-illustration.tsx | 26 ++++ .../transaction/transaction-form.tsx | 2 +- 8 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 apps/mobile/app/onboarding/_layout.tsx create mode 100644 apps/mobile/app/onboarding/step-one.tsx create mode 100644 apps/mobile/app/onboarding/step-two.tsx create mode 100644 apps/mobile/components/svg-assets/onboard-illustration.tsx diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index cf4a46bc..6478a99c 100644 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -2,7 +2,7 @@ import { BackButton } from '@/components/common/back-button' import { Button } from '@/components/ui/button' import { useColorScheme } from '@/hooks/useColorScheme' import { theme } from '@/lib/theme' -import { useAuth } from '@clerk/clerk-expo' +import { useUser } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { Link, Redirect, SplashScreen, Stack } from 'expo-router' @@ -10,7 +10,7 @@ import { PlusIcon } from 'lucide-react-native' import { useEffect } from 'react' export default function AuthenticatedLayout() { - const { isLoaded, isSignedIn } = useAuth() + const { user, isLoaded, isSignedIn } = useUser() const { colorScheme } = useColorScheme() const { i18n } = useLingui() @@ -24,6 +24,10 @@ export default function AuthenticatedLayout() { return } + if (!user?.unsafeMetadata?.onboardedAt && isLoaded) { + return + } + return ( , + headerTitle: '', + }} + > + null, + }} + /> + + + ) +} diff --git a/apps/mobile/app/onboarding/step-one.tsx b/apps/mobile/app/onboarding/step-one.tsx new file mode 100644 index 00000000..deb0ed35 --- /dev/null +++ b/apps/mobile/app/onboarding/step-one.tsx @@ -0,0 +1,40 @@ +import { OnboardIllustration } from '@/components/svg-assets/onboard-illustration' +import { Button } from '@/components/ui/button' +import { Text } from '@/components/ui/text' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { Link } from 'expo-router' +import { ArrowRightIcon } from 'lucide-react-native' +import { ScrollView, View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +export default function StepOneScreen() { + const { i18n } = useLingui() + const { bottom } = useSafeAreaInsets() + + return ( + + + + {t(i18n)`Welcome to 6pm!`} + + + {t(i18n)`Get started by setting your monthly budget.`} + + + + + + + + ) +} diff --git a/apps/mobile/app/onboarding/step-two.tsx b/apps/mobile/app/onboarding/step-two.tsx new file mode 100644 index 00000000..91388d60 --- /dev/null +++ b/apps/mobile/app/onboarding/step-two.tsx @@ -0,0 +1,133 @@ +import { SubmitButton } from '@/components/form-fields/submit-button' +import { NumericPad } from '@/components/numeric-pad' +import { TransactionAmount } from '@/components/transaction/transaction-form' +import { Text } from '@/components/ui/text' +import { useCreateBudget } from '@/stores/budget/hooks' +import { useDefaultCurrency } from '@/stores/user-settings/hooks' +import { BudgetPeriodTypeSchema, BudgetTypeSchema } from '@6pm/validation' +import { useUser } from '@clerk/clerk-expo' +import { zodResolver } from '@hookform/resolvers/zod' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { createId } from '@paralleldrive/cuid2' +import * as Haptics from 'expo-haptics' +import { useRouter } from 'expo-router' +import { + Controller, + FormProvider, + type UseFormReturn, + useForm, + useWatch, +} from 'react-hook-form' +import { ScrollView, View } from 'react-native' +import { z } from 'zod' + +const zOnboardBudgetForm = z.object({ + amount: z.number(), + currency: z.string(), +}) + +type OnboardBudgetFormValues = z.infer + +export function FormSubmitButton({ + form, + onSubmit, +}: { + form: UseFormReturn + onSubmit: (data: OnboardBudgetFormValues) => void +}) { + const { i18n } = useLingui() + const amount = useWatch({ name: 'amount' }) + + return ( + + {t(i18n)`Set Budget`} + + ) +} + +export default function StepTwoScreen() { + const { i18n } = useLingui() + const defaultCurrency = useDefaultCurrency() + const { mutateAsync } = useCreateBudget() + const { user } = useUser() + const router = useRouter() + + const form = useForm({ + resolver: zodResolver(zOnboardBudgetForm), + defaultValues: { + amount: 0, + currency: defaultCurrency, + }, + }) + + async function handleSubmit(data: OnboardBudgetFormValues) { + await mutateAsync({ + data: { + name: t(i18n)`Monthly budget`, + description: '', + preferredCurrency: data.currency, + type: BudgetTypeSchema.Enum.SPENDING, + period: { + id: createId(), + amount: data.amount, + type: BudgetPeriodTypeSchema.Enum.MONTHLY, + }, + }, + id: createId(), + }).catch(() => { + // ignore + }) + + await user?.update({ + unsafeMetadata: { + onboardedAt: new Date().toISOString(), + }, + }) + + router.replace('/') + } + + return ( + + + + + {t(i18n)`Set your monthly spending goal`} + + + {t( + i18n, + )`If you're not sure, start with how much you usually spend per month.`} + + + + + + {t(i18n)`* you can always change this later.`} + + + + + ( + + )} + /> + + + + ) +} diff --git a/apps/mobile/components/budget/budget-item.tsx b/apps/mobile/components/budget/budget-item.tsx index d339842e..ef5d7ebd 100644 --- a/apps/mobile/components/budget/budget-item.tsx +++ b/apps/mobile/components/budget/budget-item.tsx @@ -105,7 +105,7 @@ export const BudgetItem: FC = ({ budget }) => { - + ( + + + + + + + + + + + + + + + + + + +) diff --git a/apps/mobile/components/transaction/transaction-form.tsx b/apps/mobile/components/transaction/transaction-form.tsx index 6eb4a4bf..f40aec78 100644 --- a/apps/mobile/components/transaction/transaction-form.tsx +++ b/apps/mobile/components/transaction/transaction-form.tsx @@ -42,7 +42,7 @@ type TransactionFormProps = { sideOffset?: number } -function TransactionAmount() { +export function TransactionAmount() { const { colorScheme } = useColorScheme() const sheetRef = useRef(null) const [amount] = useWatch({ name: ['amount'] })