From b28849e7a5be70551cbd48c623969cd53aed879d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BB=91c=20Kh=C3=A1nh?= Date: Fri, 21 Jun 2024 16:21:33 +0700 Subject: [PATCH] feat(mobile): [Wallet] add basic account form with icon and currency input (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit
🎥 Video uploaded on Graphite:
Resolves https://github.com/sixpm-ai/6pm/issues/8 --- apps/mobile/app/(app)/(tabs)/settings.tsx | 2 +- apps/mobile/app/(app)/_layout.tsx | 61 +- apps/mobile/app/(app)/language.tsx | 16 +- apps/mobile/app/(app)/wallet/accounts.tsx | 42 + apps/mobile/app/(app)/wallet/new-account.tsx | 62 + apps/mobile/app/(aux)/_layout.tsx | 45 + apps/mobile/app/(aux)/privacy-policy.tsx | 6 +- apps/mobile/app/(aux)/terms-of-service.tsx | 4 +- apps/mobile/app/_layout.tsx | 34 +- apps/mobile/components/auth/auth-email.tsx | 12 +- apps/mobile/components/auth/auth-social.tsx | 15 +- .../components/common/add-new-button.tsx | 26 + apps/mobile/components/common/back-button.tsx | 15 + .../components/common/currency-sheet.tsx | 59 + .../mobile/components/common/generic-icon.tsx | 18 + .../components/common/icon-grid-sheet.tsx | 36 + apps/mobile/components/common/menu-item.tsx | 2 +- .../components/form-fields/input-field.tsx | 91 +- .../components/navigation/TabBarIcon.tsx | 12 - apps/mobile/components/ui/input.tsx | 4 +- .../mobile/components/wallet/account-form.tsx | 76 ++ .../wallet/select-account-icon-field.tsx | 62 + .../wallet/select-currency-field.tsx | 60 + .../components/wallet/wallet-account-item.tsx | 35 + apps/mobile/lib/icons/wallet-icons.ts | 25 + apps/mobile/mutations/user.ts | 15 +- apps/mobile/mutations/wallet.ts | 38 + apps/mobile/package.json | 3 + apps/mobile/queries/wallet.ts | 21 + packages/currency/README.md | 1 + packages/currency/package.json | 12 + packages/currency/src/currencies.json | 1073 +++++++++++++++++ packages/currency/src/index.ts | 3 + packages/validation/src/transaction.zod.ts | 4 +- packages/validation/src/wallet.zod.ts | 5 + pnpm-lock.yaml | 59 + 36 files changed, 1931 insertions(+), 123 deletions(-) create mode 100644 apps/mobile/app/(app)/wallet/accounts.tsx create mode 100644 apps/mobile/app/(app)/wallet/new-account.tsx create mode 100644 apps/mobile/app/(aux)/_layout.tsx create mode 100644 apps/mobile/components/common/add-new-button.tsx create mode 100644 apps/mobile/components/common/back-button.tsx create mode 100644 apps/mobile/components/common/currency-sheet.tsx create mode 100644 apps/mobile/components/common/generic-icon.tsx create mode 100644 apps/mobile/components/common/icon-grid-sheet.tsx delete mode 100644 apps/mobile/components/navigation/TabBarIcon.tsx create mode 100644 apps/mobile/components/wallet/account-form.tsx create mode 100644 apps/mobile/components/wallet/select-account-icon-field.tsx create mode 100644 apps/mobile/components/wallet/select-currency-field.tsx create mode 100644 apps/mobile/components/wallet/wallet-account-item.tsx create mode 100644 apps/mobile/lib/icons/wallet-icons.ts create mode 100644 apps/mobile/mutations/wallet.ts create mode 100644 apps/mobile/queries/wallet.ts create mode 100644 packages/currency/README.md create mode 100644 packages/currency/package.json create mode 100644 packages/currency/src/currencies.json create mode 100644 packages/currency/src/index.ts diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index 35af9c25..71aa9188 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -82,7 +82,7 @@ export default function SettingsScreen() { {t(i18n)`General`} - + + , + }}> + + ( + + + + ) + }} + /> + diff --git a/apps/mobile/app/(app)/language.tsx b/apps/mobile/app/(app)/language.tsx index 8279e889..371e52ee 100644 --- a/apps/mobile/app/(app)/language.tsx +++ b/apps/mobile/app/(app)/language.tsx @@ -1,5 +1,4 @@ import { MenuItem } from '@/components/common/menu-item' -import { Text } from '@/components/ui/text' import { useLocale } from '@/locales/provider' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' @@ -14,12 +13,13 @@ export default function LanguageScreen() { return ( - - {t(i18n)`Language`} - } + rightSection={ + language === 'en' && ( + + ) + } onPress={() => { setLanguage('en') router.back() @@ -27,7 +27,11 @@ export default function LanguageScreen() { /> } + rightSection={ + language === 'vi' && ( + + ) + } onPress={() => { setLanguage('vi') router.back() diff --git a/apps/mobile/app/(app)/wallet/accounts.tsx b/apps/mobile/app/(app)/wallet/accounts.tsx new file mode 100644 index 00000000..10e6cf1e --- /dev/null +++ b/apps/mobile/app/(app)/wallet/accounts.tsx @@ -0,0 +1,42 @@ +import { AddNewButton } from '@/components/common/add-new-button' +import { Text } from '@/components/ui/text' +import { WalletAccountItem } from '@/components/wallet/wallet-account-item' +import { useWallets } from '@/queries/wallet' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { useRouter } from 'expo-router' +import { FlatList } from 'react-native' + +export default function WalletAccountsScreen() { + const { i18n } = useLingui() + const { data: walletAccounts, isLoading, refetch } = useWallets() + const router = useRouter() + return ( + ( + + data={item as any} + /> + )} + keyExtractor={(item) => item.id} + refreshing={isLoading} + onRefresh={refetch} + ListFooterComponent={ + router.push('/wallet/new-account')} + /> + } + ListEmptyComponent={ + + {t(i18n)`empty`} + + } + /> + ) +} diff --git a/apps/mobile/app/(app)/wallet/new-account.tsx b/apps/mobile/app/(app)/wallet/new-account.tsx new file mode 100644 index 00000000..d6cb65c2 --- /dev/null +++ b/apps/mobile/app/(app)/wallet/new-account.tsx @@ -0,0 +1,62 @@ +import { AccountForm } from '@/components/wallet/account-form' +import { createWallet } from '@/mutations/wallet' +import { walletQueries } from '@/queries/wallet' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useRouter } from 'expo-router' +import { Alert, ScrollView } from 'react-native' + +export default function NewAccountScreen() { + const queryClient = useQueryClient() + const router = useRouter() + const { mutateAsync } = useMutation({ + mutationFn: createWallet, + async onMutate(newWallet) { + // Cancel any outgoing refetches + // (so they don't overwrite our optimistic update) + await queryClient.cancelQueries({ + queryKey: walletQueries.list().queryKey, + }) + // Snapshot the previous value + const previousWallets = queryClient.getQueryData( + walletQueries.list().queryKey, + ) + // Optimistically update to the new value + // biome-ignore lint/suspicious/noExplicitAny: + queryClient.setQueryData(walletQueries.list().queryKey, (old: any) => [ + ...old, + newWallet, + ]) + // Return a context object with the snapshotted value + return { previousWallets } + }, + onError(error, _, context) { + Alert.alert(error.message) + // use the context returned from onMutate to rollback + queryClient.setQueryData( + walletQueries.list().queryKey, + context?.previousWallets, + ) + }, + onSuccess() { + router.back() + }, + async onSettled() { + // Always refetch after error or success: + await queryClient.invalidateQueries({ + queryKey: walletQueries._def, + }) + }, + throwOnError: true, + }) + + return ( + + + + ) +} diff --git a/apps/mobile/app/(aux)/_layout.tsx b/apps/mobile/app/(aux)/_layout.tsx new file mode 100644 index 00000000..6e0024d1 --- /dev/null +++ b/apps/mobile/app/(aux)/_layout.tsx @@ -0,0 +1,45 @@ +import { BackButton } from '@/components/common/back-button' +import { useColorScheme } from '@/hooks/useColorScheme' +import { theme } from '@/lib/theme' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { Stack } from 'expo-router' + +export default function AuxiliaryLayout() { + const { colorScheme } = useColorScheme() + const { i18n } = useLingui() + return ( + , + }} + > + + + + ) +} diff --git a/apps/mobile/app/(aux)/privacy-policy.tsx b/apps/mobile/app/(aux)/privacy-policy.tsx index 9c17a8ce..892d77b3 100644 --- a/apps/mobile/app/(aux)/privacy-policy.tsx +++ b/apps/mobile/app/(aux)/privacy-policy.tsx @@ -1,5 +1,9 @@ import { Text } from 'react-native' export default function PrivacyScreen() { - return Privacy Policy + return ( + + lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + ) } diff --git a/apps/mobile/app/(aux)/terms-of-service.tsx b/apps/mobile/app/(aux)/terms-of-service.tsx index 1791aede..38ad72e8 100644 --- a/apps/mobile/app/(aux)/terms-of-service.tsx +++ b/apps/mobile/app/(aux)/terms-of-service.tsx @@ -2,6 +2,8 @@ import { Text } from 'react-native' export default function TermsScreen() { return ( - Terms & Conditions + + lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + ) } diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 680a30cb..e6fb4869 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -11,13 +11,14 @@ import { useFonts, } from '@expo-google-fonts/be-vietnam-pro' import { SplashScreen, Stack } from 'expo-router' -import * as WebBrowser from "expo-web-browser"; +import * as WebBrowser from 'expo-web-browser' import 'react-native-reanimated' import { useWarmUpBrowser } from '@/hooks/use-warm-up-browser' import { useColorScheme } from '@/hooks/useColorScheme' import { queryClient } from '@/lib/client' import { LocaleProvider } from '@/locales/provider' +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { DarkTheme, DefaultTheme, @@ -25,6 +26,7 @@ import { } from '@react-navigation/native' import { QueryClientProvider } from '@tanstack/react-query' import { cssInterop } from 'nativewind' +import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SafeAreaProvider } from 'react-native-safe-area-context' import { Svg } from 'react-native-svg' @@ -38,7 +40,7 @@ cssInterop(Svg, { // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync() -WebBrowser.maybeCompleteAuthSession(); +WebBrowser.maybeCompleteAuthSession() // biome-ignore lint/style/useNamingConvention: export const unstable_settings = { @@ -46,7 +48,7 @@ export const unstable_settings = { } export default function RootLayout() { - useWarmUpBrowser(); + useWarmUpBrowser() const { colorScheme } = useColorScheme() const [fontsLoaded] = useFonts({ BeVietnamPro_300Light, @@ -73,20 +75,18 @@ export default function RootLayout() { value={colorScheme === 'dark' ? DarkTheme : DefaultTheme} > - - - - + + + + + + + diff --git a/apps/mobile/components/auth/auth-email.tsx b/apps/mobile/components/auth/auth-email.tsx index 56d3549f..a485b746 100644 --- a/apps/mobile/components/auth/auth-email.tsx +++ b/apps/mobile/components/auth/auth-email.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { View } from 'react-native' -import { useCreateUserMutation } from '@/mutations/user' +import { createUser } from '@/mutations/user' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { XCircleIcon } from 'lucide-react-native' @@ -33,8 +33,6 @@ export function AuthEmail() { const [verifying, setVerifying] = useState(false) const [mode, setMode] = useState<'signUp' | 'signIn'>('signUp') - const { mutateAsync: createUser } = useCreateUserMutation() - const { isLoaded: isSignUpLoaded, signUp, @@ -176,10 +174,10 @@ export function AuthEmail() { autoFocus onEndEditing={verifyEmailForm.handleSubmit(onVerify)} /> - - {mode === 'signUp' ? t(i18n)`Sign up` : t(i18n)`Sign in`} + + + {mode === 'signUp' ? t(i18n)`Sign up` : t(i18n)`Sign in`} + diff --git a/apps/mobile/components/auth/auth-social.tsx b/apps/mobile/components/auth/auth-social.tsx index 80ca4fd8..9461f604 100644 --- a/apps/mobile/components/auth/auth-social.tsx +++ b/apps/mobile/components/auth/auth-social.tsx @@ -1,4 +1,4 @@ -import { useCreateUserMutation } from '@/mutations/user' +import { createUser } from '@/mutations/user' import { useOAuth } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' @@ -16,7 +16,6 @@ type AuthSocialProps = { export function AuthSocial({ label, icon: Icon, strategy }: AuthSocialProps) { const { startOAuthFlow } = useOAuth({ strategy }) - const { mutateAsync: createUser } = useCreateUserMutation() const onPress = async () => { try { @@ -25,10 +24,14 @@ export function AuthSocial({ label, icon: Icon, strategy }: AuthSocialProps) { if (createdSessionId) { setActive?.({ session: createdSessionId }) if (signUp?.createdUserId) { - setTimeout(async () => await createUser({ - email: signUp.emailAddress!, - name: signUp.firstName ?? '', - }), 1000) + setTimeout( + async () => + await createUser({ + email: signUp.emailAddress!, + name: signUp.firstName ?? '', + }), + 1000, + ) } } else { // Use signIn or signUp for next steps such as MFA diff --git a/apps/mobile/components/common/add-new-button.tsx b/apps/mobile/components/common/add-new-button.tsx new file mode 100644 index 00000000..d3ff8593 --- /dev/null +++ b/apps/mobile/components/common/add-new-button.tsx @@ -0,0 +1,26 @@ +import { cn } from "@/lib/utils" +import { PlusCircleIcon } from "lucide-react-native" +import { Button } from "../ui/button" +import { Text } from "../ui/text" + +type AddNewButtonProps = { + label: string + className?: string + onPress?: () => void +} + +export function AddNewButton( + { label, className, onPress }: AddNewButtonProps, +) { + return ( + + ) +} \ No newline at end of file diff --git a/apps/mobile/components/common/back-button.tsx b/apps/mobile/components/common/back-button.tsx new file mode 100644 index 00000000..9d72bb18 --- /dev/null +++ b/apps/mobile/components/common/back-button.tsx @@ -0,0 +1,15 @@ +import { useRouter } from "expo-router"; +import { ArrowLeftIcon } from "lucide-react-native"; +import { Button } from "../ui/button"; + +export function BackButton() { + const router = useRouter() + if (!router.canGoBack) { + return null + } + return ( + + ) +} \ No newline at end of file diff --git a/apps/mobile/components/common/currency-sheet.tsx b/apps/mobile/components/common/currency-sheet.tsx new file mode 100644 index 00000000..6b216866 --- /dev/null +++ b/apps/mobile/components/common/currency-sheet.tsx @@ -0,0 +1,59 @@ +import { currencies } from "@6pm/currency"; +import { BottomSheetFlatList, BottomSheetTextInput } from "@gorhom/bottom-sheet"; +import { t } from "@lingui/macro"; +import { useLingui } from "@lingui/react"; +import { SearchIcon } from "lucide-react-native"; +import { useState } from "react"; +import { View } from "react-native"; +import { Text } from "../ui/text"; +import { MenuItem } from "./menu-item"; + +type CurrencySheetListProps = { + onSelect: (currency: typeof currencies[number]) => void; + value: string; +} + +export function CurrencySheetList({ onSelect, value }: CurrencySheetListProps) { + const { i18n } = useLingui() + const [searchValue, setSearchValue] = useState('') + + const filteredCurrencies = currencies.filter((currency) => { + const search = searchValue.toLowerCase() + return currency.name.toLowerCase().includes(search) || currency.code.toLowerCase().includes(search) + }) + + return ( + i.code} + contentContainerClassName="pb-8" + stickyHeaderIndices={[0]} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" + ListHeaderComponent={ + + + + + } + renderItem={({ item }) => ( + onSelect(item)} + className={item.code === value ? 'bg-muted' : ''} + rightSection={ + + {item.code} + + } + /> + )} + /> + ) +} diff --git a/apps/mobile/components/common/generic-icon.tsx b/apps/mobile/components/common/generic-icon.tsx new file mode 100644 index 00000000..6c017623 --- /dev/null +++ b/apps/mobile/components/common/generic-icon.tsx @@ -0,0 +1,18 @@ +import { type LucideProps, icons } from 'lucide-react-native' +import type { FC } from 'react' + +/** +* TODO: Only export the icons that are used to reduce the bundle size +*/ + +const GenericIcon: FC< + LucideProps & { + name: keyof typeof icons + } +> = ({ name, ...props }) => { + const LucideIcon = icons[name] + + return +} + +export default GenericIcon diff --git a/apps/mobile/components/common/icon-grid-sheet.tsx b/apps/mobile/components/common/icon-grid-sheet.tsx new file mode 100644 index 00000000..5a3779f6 --- /dev/null +++ b/apps/mobile/components/common/icon-grid-sheet.tsx @@ -0,0 +1,36 @@ +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import { Button } from '../ui/button' +import GenericIcon from './generic-icon' + +type IconGridSheetProps = { + icons: string[] + onSelect: (icon: string) => void + value: string +} + +export function IconGridSheet({ icons, onSelect, value }: IconGridSheetProps) { + return ( + i} + contentContainerClassName="pb-8 px-4 gap-2" + columnWrapperClassName="gap-2" + showsVerticalScrollIndicator={false} + showsHorizontalScrollIndicator={false} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" + renderItem={({ item }) => ( + + )} + /> + ) +} diff --git a/apps/mobile/components/common/menu-item.tsx b/apps/mobile/components/common/menu-item.tsx index bb318cec..ddebda30 100644 --- a/apps/mobile/components/common/menu-item.tsx +++ b/apps/mobile/components/common/menu-item.tsx @@ -23,7 +23,7 @@ export const MenuItem = forwardRef(function( ref={ref} disabled={disabled} className={cn( - "flex flex-row items-center gap-4 px-6 justify-between h-12 active:bg-muted", + "flex flex-row items-center gap-4 px-6 justify-between h-14 active:bg-muted", disabled && "opacity-50", className, )} diff --git a/apps/mobile/components/form-fields/input-field.tsx b/apps/mobile/components/form-fields/input-field.tsx index aa761d24..30b62a37 100644 --- a/apps/mobile/components/form-fields/input-field.tsx +++ b/apps/mobile/components/form-fields/input-field.tsx @@ -1,8 +1,9 @@ import { cn } from '@/lib/utils' import { useController } from 'react-hook-form' -import { Text, type TextInputProps, View } from 'react-native' +import { Text, type TextInputProps, View, type TextInput } from 'react-native' import { Input } from '../ui/input' import { Label } from '../ui/label' +import { forwardRef } from 'react' type InputFieldProps = TextInputProps & { name: string @@ -11,47 +12,53 @@ type InputFieldProps = TextInputProps & { rightSection?: React.ReactNode } -export const InputField: React.FC = ({ - name, - label, - leftSection, - rightSection, - className, - ...props -}) => { - const { - field: { onChange, onBlur, value }, - fieldState, - } = useController({ name }) - return ( - - {!!label && } - - {leftSection && ( - - {leftSection} - - )} - , + ) => { + const { + field: { onChange, onBlur, value }, + fieldState, + } = useController({ name }) + return ( + + {!!label && } + + {leftSection && ( + + {leftSection} + + )} + + {rightSection && ( + + {rightSection} + )} - {...props} - /> - {rightSection && ( - - {rightSection} - + + {!!fieldState.error && ( + {fieldState.error.message} )} - {!!fieldState.error && ( - {fieldState.error.message} - )} - - ) -} + ) + }, +) diff --git a/apps/mobile/components/navigation/TabBarIcon.tsx b/apps/mobile/components/navigation/TabBarIcon.tsx deleted file mode 100644 index f06993bf..00000000 --- a/apps/mobile/components/navigation/TabBarIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ - -import Ionicons from '@expo/vector-icons/Ionicons' -import type { IconProps } from '@expo/vector-icons/build/createIconSet' -import type { ComponentProps } from 'react' - -export function TabBarIcon({ - style, - ...rest -}: IconProps['name']>) { - return -} diff --git a/apps/mobile/components/ui/input.tsx b/apps/mobile/components/ui/input.tsx index f44ede87..f525c444 100644 --- a/apps/mobile/components/ui/input.tsx +++ b/apps/mobile/components/ui/input.tsx @@ -10,11 +10,11 @@ const Input = React.forwardRef< ); diff --git a/apps/mobile/components/wallet/account-form.tsx b/apps/mobile/components/wallet/account-form.tsx new file mode 100644 index 00000000..bf21de26 --- /dev/null +++ b/apps/mobile/components/wallet/account-form.tsx @@ -0,0 +1,76 @@ +import { type AccountFormValues, zAccountFormValues } from '@6pm/validation' +import { zodResolver } from '@hookform/resolvers/zod' +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { useRef } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { View } from 'react-native' +import type { TextInput } from 'react-native' +import { InputField } from '../form-fields/input-field' +import { SubmitButton } from '../form-fields/submit-button' +import { Text } from '../ui/text' +import { SelectAccountIconField } from './select-account-icon-field' +import { SelectCurrencyField } from './select-currency-field' + +type AccountFormProps = { + onSubmit: (data: AccountFormValues) => void +} + +export const AccountForm = ({ onSubmit }: AccountFormProps) => { + const { i18n } = useLingui() + const nameInputRef = useRef(null) + const balanceInputRef = useRef(null) + + const accountForm = useForm({ + resolver: zodResolver(zAccountFormValues), + defaultValues: { + name: '', + preferredCurrency: 'USD', // TODO: get from user settings + icon: 'CreditCard', + }, + }) + + return ( + + + nameInputRef.current?.focus()} + /> + } + onSubmitEditing={() => { + balanceInputRef.current?.focus() + }} + /> + balanceInputRef.current?.focus()} + /> + } + /> + + {t(i18n)`Save`} + + + + ) +} diff --git a/apps/mobile/components/wallet/select-account-icon-field.tsx b/apps/mobile/components/wallet/select-account-icon-field.tsx new file mode 100644 index 00000000..2a78874a --- /dev/null +++ b/apps/mobile/components/wallet/select-account-icon-field.tsx @@ -0,0 +1,62 @@ +import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' +import { useRef } from 'react' + +import { WALLET_ICONS } from '@/lib/icons/wallet-icons' +import { useController } from 'react-hook-form' +import { Keyboard } from 'react-native' +import GenericIcon from '../common/generic-icon' +import { IconGridSheet } from '../common/icon-grid-sheet' +import { Button } from '../ui/button' + +export function SelectAccountIconField({ + onSelect, +}: { + onSelect?: (currency: string) => void +}) { + const sheetRef = useRef(null) + const { + field: { onChange, onBlur, value }, + // fieldState, + } = useController({ name: 'icon' }) + + return ( + <> + + ( + + )} + > + { + onChange(icon) + sheetRef.current?.close() + onBlur() + onSelect?.(icon) + }} + /> + + + ) +} diff --git a/apps/mobile/components/wallet/select-currency-field.tsx b/apps/mobile/components/wallet/select-currency-field.tsx new file mode 100644 index 00000000..1409594d --- /dev/null +++ b/apps/mobile/components/wallet/select-currency-field.tsx @@ -0,0 +1,60 @@ +import { BottomSheetBackdrop, BottomSheetModal } from '@gorhom/bottom-sheet' +import { useRef } from 'react' + +import { useController } from 'react-hook-form' +import { Keyboard } from 'react-native' +import { CurrencySheetList } from '../common/currency-sheet' +import { Button } from '../ui/button' +import { Text } from '../ui/text' + +export function SelectCurrencyField({ + onSelect, +}: { + onSelect?: (currency: string) => void +}) { + const sheetRef = useRef(null) + const { + field: { onChange, onBlur, value }, + // fieldState, + } = useController({ name: 'preferredCurrency' }) + + return ( + <> + + ( + + )} + > + { + onChange(currency.code) + sheetRef.current?.close() + onBlur() + onSelect?.(currency.code) + }} + /> + + + ) +} diff --git a/apps/mobile/components/wallet/wallet-account-item.tsx b/apps/mobile/components/wallet/wallet-account-item.tsx new file mode 100644 index 00000000..b4e3cd77 --- /dev/null +++ b/apps/mobile/components/wallet/wallet-account-item.tsx @@ -0,0 +1,35 @@ +import type { UserWalletAccount } from '@6pm/api' +import { ChevronRightIcon } from 'lucide-react-native' +import type { FC } from 'react' +import { View } from 'react-native' +import GenericIcon from '../common/generic-icon' +import { MenuItem } from '../common/menu-item' +import { Text } from '../ui/text' + +type WalletAccountItemProps = { + data: UserWalletAccount & { balance: number } +} + +export const WalletAccountItem: FC = ({ data }) => { + return ( + ( + + name={data.icon as any} + className="size-6 text-foreground" + /> + )} + rightSection={ + + + {data.balance.toLocaleString()}{' '} + {data.preferredCurrency} + + + + } + /> + ) +} diff --git a/apps/mobile/lib/icons/wallet-icons.ts b/apps/mobile/lib/icons/wallet-icons.ts new file mode 100644 index 00000000..870310d6 --- /dev/null +++ b/apps/mobile/lib/icons/wallet-icons.ts @@ -0,0 +1,25 @@ +import type { icons } from 'lucide-react-native' + +export const WALLET_ICONS: Array = [ + 'WalletMinimal', + 'Coins', + 'Banknote', + 'Bitcoin', + 'CreditCard', + 'Gem', + 'HandCoins', + 'Handshake', + 'PiggyBank', + 'SmartphoneNfc', + 'BadgeCent', + 'BadgeDollarSign', + 'BadgeEuro', + 'BadgeIndianRupee', + 'BadgeJapaneseYen', + 'BadgePoundSterling', + 'BadgeRussianRuble', + 'BadgeSwissFranc', + // 'Paintbrush', + // 'BrickWall', + // 'CookingPot', +] diff --git a/apps/mobile/mutations/user.ts b/apps/mobile/mutations/user.ts index 65ef9d31..14b8920d 100644 --- a/apps/mobile/mutations/user.ts +++ b/apps/mobile/mutations/user.ts @@ -1,14 +1,9 @@ import { getHonoClient } from '@/lib/client' -import type { CreateUser } from '@6pm/api' -import { useMutation } from '@tanstack/react-query' +import type { CreateUser } from '@6pm/validation' -export function useCreateUserMutation() { - return useMutation({ - mutationFn: async (data: CreateUser) => { - const hc = await getHonoClient() - await hc.v1.users.$post({ - json: data, - }) - }, +export async function createUser(data: CreateUser) { + const hc = await getHonoClient() + await hc.v1.users.$post({ + json: data, }) } diff --git a/apps/mobile/mutations/wallet.ts b/apps/mobile/mutations/wallet.ts new file mode 100644 index 00000000..3fbd9728 --- /dev/null +++ b/apps/mobile/mutations/wallet.ts @@ -0,0 +1,38 @@ +import { getHonoClient } from '@/lib/client' +import type { AccountFormValues, UpdateWallet } from '@6pm/validation' + +export async function createWallet(data: AccountFormValues) { + const { balance, ...walletData } = data + const hc = await getHonoClient() + const result = await hc.v1.wallets.wallets.$post({ + json: walletData, + }) + if (result.ok) { + const wallet = await result.json() + await hc.v1.transactions.$post({ + json: { + amount: balance ?? 0, + walletAccountId: wallet.id, + currency: wallet.preferredCurrency, + date: new Date(), + note: 'Initial balance', + }, + }) + } + return result +} + +export async function updateWallet(id: string, data: UpdateWallet) { + const hc = await getHonoClient() + await hc.v1.wallets.wallets[':walletId'].$put({ + param: { walletId: id }, + json: data, + }) +} + +export async function deleteWallet(id: string) { + const hc = await getHonoClient() + await hc.v1.wallets.wallets[':walletId'].$delete({ + param: { walletId: id }, + }) +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 7ba6d95f..521b6a88 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -17,15 +17,18 @@ }, "dependencies": { "@6pm/api": "workspace:^", + "@6pm/currency": "workspace:^", "@6pm/validation": "workspace:^", "@clerk/clerk-expo": "^1.2.0", "@expo-google-fonts/be-vietnam-pro": "^0.2.3", "@expo/vector-icons": "^14.0.0", "@formatjs/intl-locale": "^4.0.0", "@formatjs/intl-pluralrules": "^5.2.14", + "@gorhom/bottom-sheet": "^4.6.3", "@hookform/resolvers": "^3.6.0", "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", + "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", diff --git a/apps/mobile/queries/wallet.ts b/apps/mobile/queries/wallet.ts new file mode 100644 index 00000000..511d7ac0 --- /dev/null +++ b/apps/mobile/queries/wallet.ts @@ -0,0 +1,21 @@ +import { getHonoClient } from '@/lib/client' +import { createQueryKeys } from '@lukemorales/query-key-factory' +import { useQuery } from '@tanstack/react-query' + +export const walletQueries = createQueryKeys('wallet', { + list: () => ({ + queryKey: [{}], + queryFn: async () => { + const hc = await getHonoClient() + const res = await hc.v1.wallets.wallets.$get() + if (!res.ok) { + throw new Error(await res.text()) + } + return await res.json() + }, + }), +}) + +export function useWallets() { + return useQuery(walletQueries.list()) +} diff --git a/packages/currency/README.md b/packages/currency/README.md new file mode 100644 index 00000000..24214aea --- /dev/null +++ b/packages/currency/README.md @@ -0,0 +1 @@ +# @6pm/currency diff --git a/packages/currency/package.json b/packages/currency/package.json new file mode 100644 index 00000000..83817427 --- /dev/null +++ b/packages/currency/package.json @@ -0,0 +1,12 @@ +{ + "name": "@6pm/currency", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/currency/src/currencies.json b/packages/currency/src/currencies.json new file mode 100644 index 00000000..87d0340b --- /dev/null +++ b/packages/currency/src/currencies.json @@ -0,0 +1,1073 @@ +[ + { + "name": "US Dollar", + "symbol": "$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "USD", + "namePlural": "US dollars" + }, + { + "name": "Canadian Dollar", + "symbol": "CA$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "CAD", + "namePlural": "Canadian dollars" + }, + { + "name": "Euro", + "symbol": "€", + "symbolNative": "€", + "decimalDigits": 2, + "rounding": 0, + "code": "EUR", + "namePlural": "euros" + }, + { + "name": "United Arab Emirates Dirham", + "symbol": "AED", + "symbolNative": "د.إ.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "AED", + "namePlural": "UAE dirhams" + }, + { + "name": "Afghan Afghani", + "symbol": "Af", + "symbolNative": "؋", + "decimalDigits": 0, + "rounding": 0, + "code": "AFN", + "namePlural": "Afghan Afghanis" + }, + { + "name": "Albanian Lek", + "symbol": "ALL", + "symbolNative": "Lek", + "decimalDigits": 0, + "rounding": 0, + "code": "ALL", + "namePlural": "Albanian lekë" + }, + { + "name": "Armenian Dram", + "symbol": "AMD", + "symbolNative": "դր.", + "decimalDigits": 0, + "rounding": 0, + "code": "AMD", + "namePlural": "Armenian drams" + }, + { + "name": "Argentine Peso", + "symbol": "AR$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "ARS", + "namePlural": "Argentine pesos" + }, + { + "name": "Australian Dollar", + "symbol": "AU$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "AUD", + "namePlural": "Australian dollars" + }, + { + "name": "Azerbaijani Manat", + "symbol": "man.", + "symbolNative": "ман.", + "decimalDigits": 2, + "rounding": 0, + "code": "AZN", + "namePlural": "Azerbaijani manats" + }, + { + "name": "Bosnia-Herzegovina Convertible Mark", + "symbol": "KM", + "symbolNative": "KM", + "decimalDigits": 2, + "rounding": 0, + "code": "BAM", + "namePlural": "Bosnia-Herzegovina convertible marks" + }, + { + "name": "Bangladeshi Taka", + "symbol": "Tk", + "symbolNative": "৳", + "decimalDigits": 2, + "rounding": 0, + "code": "BDT", + "namePlural": "Bangladeshi takas" + }, + { + "name": "Bulgarian Lev", + "symbol": "BGN", + "symbolNative": "лв.", + "decimalDigits": 2, + "rounding": 0, + "code": "BGN", + "namePlural": "Bulgarian leva" + }, + { + "name": "Bahraini Dinar", + "symbol": "BD", + "symbolNative": "د.ب.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "BHD", + "namePlural": "Bahraini dinars" + }, + { + "name": "Burundian Franc", + "symbol": "FBu", + "symbolNative": "FBu", + "decimalDigits": 0, + "rounding": 0, + "code": "BIF", + "namePlural": "Burundian francs" + }, + { + "name": "Brunei Dollar", + "symbol": "BN$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "BND", + "namePlural": "Brunei dollars" + }, + { + "name": "Bolivian Boliviano", + "symbol": "Bs", + "symbolNative": "Bs", + "decimalDigits": 2, + "rounding": 0, + "code": "BOB", + "namePlural": "Bolivian bolivianos" + }, + { + "name": "Brazilian Real", + "symbol": "R$", + "symbolNative": "R$", + "decimalDigits": 2, + "rounding": 0, + "code": "BRL", + "namePlural": "Brazilian reals" + }, + { + "name": "Botswanan Pula", + "symbol": "BWP", + "symbolNative": "P", + "decimalDigits": 2, + "rounding": 0, + "code": "BWP", + "namePlural": "Botswanan pulas" + }, + { + "name": "Belarusian Ruble", + "symbol": "Br", + "symbolNative": "руб.", + "decimalDigits": 2, + "rounding": 0, + "code": "BYN", + "namePlural": "Belarusian rubles" + }, + { + "name": "Belize Dollar", + "symbol": "BZ$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "BZD", + "namePlural": "Belize dollars" + }, + { + "name": "Congolese Franc", + "symbol": "CDF", + "symbolNative": "FrCD", + "decimalDigits": 2, + "rounding": 0, + "code": "CDF", + "namePlural": "Congolese francs" + }, + { + "name": "Swiss Franc", + "symbol": "CHF", + "symbolNative": "CHF", + "decimalDigits": 2, + "rounding": 0.05, + "code": "CHF", + "namePlural": "Swiss francs" + }, + { + "name": "Chilean Peso", + "symbol": "CL$", + "symbolNative": "$", + "decimalDigits": 0, + "rounding": 0, + "code": "CLP", + "namePlural": "Chilean pesos" + }, + { + "name": "Chinese Yuan", + "symbol": "CN¥", + "symbolNative": "CN¥", + "decimalDigits": 2, + "rounding": 0, + "code": "CNY", + "namePlural": "Chinese yuan" + }, + { + "name": "Colombian Peso", + "symbol": "CO$", + "symbolNative": "$", + "decimalDigits": 0, + "rounding": 0, + "code": "COP", + "namePlural": "Colombian pesos" + }, + { + "name": "Costa Rican Colón", + "symbol": "₡", + "symbolNative": "₡", + "decimalDigits": 0, + "rounding": 0, + "code": "CRC", + "namePlural": "Costa Rican colóns" + }, + { + "name": "Cape Verdean Escudo", + "symbol": "CV$", + "symbolNative": "CV$", + "decimalDigits": 2, + "rounding": 0, + "code": "CVE", + "namePlural": "Cape Verdean escudos" + }, + { + "name": "Czech Republic Koruna", + "symbol": "Kč", + "symbolNative": "Kč", + "decimalDigits": 2, + "rounding": 0, + "code": "CZK", + "namePlural": "Czech Republic korunas" + }, + { + "name": "Djiboutian Franc", + "symbol": "Fdj", + "symbolNative": "Fdj", + "decimalDigits": 0, + "rounding": 0, + "code": "DJF", + "namePlural": "Djiboutian francs" + }, + { + "name": "Danish Krone", + "symbol": "Dkr", + "symbolNative": "kr", + "decimalDigits": 2, + "rounding": 0, + "code": "DKK", + "namePlural": "Danish kroner" + }, + { + "name": "Dominican Peso", + "symbol": "RD$", + "symbolNative": "RD$", + "decimalDigits": 2, + "rounding": 0, + "code": "DOP", + "namePlural": "Dominican pesos" + }, + { + "name": "Algerian Dinar", + "symbol": "DA", + "symbolNative": "د.ج.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "DZD", + "namePlural": "Algerian dinars" + }, + { + "name": "Estonian Kroon", + "symbol": "Ekr", + "symbolNative": "kr", + "decimalDigits": 2, + "rounding": 0, + "code": "EEK", + "namePlural": "Estonian kroons" + }, + { + "name": "Egyptian Pound", + "symbol": "EGP", + "symbolNative": "ج.م.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "EGP", + "namePlural": "Egyptian pounds" + }, + { + "name": "Eritrean Nakfa", + "symbol": "Nfk", + "symbolNative": "Nfk", + "decimalDigits": 2, + "rounding": 0, + "code": "ERN", + "namePlural": "Eritrean nakfas" + }, + { + "name": "Ethiopian Birr", + "symbol": "Br", + "symbolNative": "Br", + "decimalDigits": 2, + "rounding": 0, + "code": "ETB", + "namePlural": "Ethiopian birrs" + }, + { + "name": "British Pound Sterling", + "symbol": "£", + "symbolNative": "£", + "decimalDigits": 2, + "rounding": 0, + "code": "GBP", + "namePlural": "British pounds sterling" + }, + { + "name": "Georgian Lari", + "symbol": "GEL", + "symbolNative": "GEL", + "decimalDigits": 2, + "rounding": 0, + "code": "GEL", + "namePlural": "Georgian laris" + }, + { + "name": "Ghanaian Cedi", + "symbol": "GH₵", + "symbolNative": "GH₵", + "decimalDigits": 2, + "rounding": 0, + "code": "GHS", + "namePlural": "Ghanaian cedis" + }, + { + "name": "Guinean Franc", + "symbol": "FG", + "symbolNative": "FG", + "decimalDigits": 0, + "rounding": 0, + "code": "GNF", + "namePlural": "Guinean francs" + }, + { + "name": "Guatemalan Quetzal", + "symbol": "GTQ", + "symbolNative": "Q", + "decimalDigits": 2, + "rounding": 0, + "code": "GTQ", + "namePlural": "Guatemalan quetzals" + }, + { + "name": "Hong Kong Dollar", + "symbol": "HK$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "HKD", + "namePlural": "Hong Kong dollars" + }, + { + "name": "Honduran Lempira", + "symbol": "HNL", + "symbolNative": "L", + "decimalDigits": 2, + "rounding": 0, + "code": "HNL", + "namePlural": "Honduran lempiras" + }, + { + "name": "Croatian Kuna", + "symbol": "kn", + "symbolNative": "kn", + "decimalDigits": 2, + "rounding": 0, + "code": "HRK", + "namePlural": "Croatian kunas" + }, + { + "name": "Hungarian Forint", + "symbol": "Ft", + "symbolNative": "Ft", + "decimalDigits": 0, + "rounding": 0, + "code": "HUF", + "namePlural": "Hungarian forints" + }, + { + "name": "Indonesian Rupiah", + "symbol": "Rp", + "symbolNative": "Rp", + "decimalDigits": 0, + "rounding": 0, + "code": "IDR", + "namePlural": "Indonesian rupiahs" + }, + { + "name": "Israeli New Sheqel", + "symbol": "₪", + "symbolNative": "₪", + "decimalDigits": 2, + "rounding": 0, + "code": "ILS", + "namePlural": "Israeli new sheqels" + }, + { + "name": "Indian Rupee", + "symbol": "Rs", + "symbolNative": "টকা", + "decimalDigits": 2, + "rounding": 0, + "code": "INR", + "namePlural": "Indian rupees" + }, + { + "name": "Iraqi Dinar", + "symbol": "IQD", + "symbolNative": "د.ع.‏", + "decimalDigits": 0, + "rounding": 0, + "code": "IQD", + "namePlural": "Iraqi dinars" + }, + { + "name": "Iranian Rial", + "symbol": "IRR", + "symbolNative": "﷼", + "decimalDigits": 0, + "rounding": 0, + "code": "IRR", + "namePlural": "Iranian rials" + }, + { + "name": "Icelandic Króna", + "symbol": "Ikr", + "symbolNative": "kr", + "decimalDigits": 0, + "rounding": 0, + "code": "ISK", + "namePlural": "Icelandic krónur" + }, + { + "name": "Jamaican Dollar", + "symbol": "J$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "JMD", + "namePlural": "Jamaican dollars" + }, + { + "name": "Jordanian Dinar", + "symbol": "JD", + "symbolNative": "د.أ.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "JOD", + "namePlural": "Jordanian dinars" + }, + { + "name": "Japanese Yen", + "symbol": "¥", + "symbolNative": "¥", + "decimalDigits": 0, + "rounding": 0, + "code": "JPY", + "namePlural": "Japanese yen" + }, + { + "name": "Kenyan Shilling", + "symbol": "Ksh", + "symbolNative": "Ksh", + "decimalDigits": 2, + "rounding": 0, + "code": "KES", + "namePlural": "Kenyan shillings" + }, + { + "name": "Cambodian Riel", + "symbol": "KHR", + "symbolNative": "៛", + "decimalDigits": 2, + "rounding": 0, + "code": "KHR", + "namePlural": "Cambodian riels" + }, + { + "name": "Comorian Franc", + "symbol": "CF", + "symbolNative": "FC", + "decimalDigits": 0, + "rounding": 0, + "code": "KMF", + "namePlural": "Comorian francs" + }, + { + "name": "South Korean Won", + "symbol": "₩", + "symbolNative": "₩", + "decimalDigits": 0, + "rounding": 0, + "code": "KRW", + "namePlural": "South Korean won" + }, + { + "name": "Kuwaiti Dinar", + "symbol": "KD", + "symbolNative": "د.ك.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "KWD", + "namePlural": "Kuwaiti dinars" + }, + { + "name": "Kazakhstani Tenge", + "symbol": "KZT", + "symbolNative": "тңг.", + "decimalDigits": 2, + "rounding": 0, + "code": "KZT", + "namePlural": "Kazakhstani tenges" + }, + { + "name": "Lebanese Pound", + "symbol": "LB£", + "symbolNative": "ل.ل.‏", + "decimalDigits": 0, + "rounding": 0, + "code": "LBP", + "namePlural": "Lebanese pounds" + }, + { + "name": "Sri Lankan Rupee", + "symbol": "SLRs", + "symbolNative": "SL Re", + "decimalDigits": 2, + "rounding": 0, + "code": "LKR", + "namePlural": "Sri Lankan rupees" + }, + { + "name": "Lithuanian Litas", + "symbol": "Lt", + "symbolNative": "Lt", + "decimalDigits": 2, + "rounding": 0, + "code": "LTL", + "namePlural": "Lithuanian litai" + }, + { + "name": "Latvian Lats", + "symbol": "Ls", + "symbolNative": "Ls", + "decimalDigits": 2, + "rounding": 0, + "code": "LVL", + "namePlural": "Latvian lati" + }, + { + "name": "Libyan Dinar", + "symbol": "LD", + "symbolNative": "د.ل.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "LYD", + "namePlural": "Libyan dinars" + }, + { + "name": "Moroccan Dirham", + "symbol": "MAD", + "symbolNative": "د.م.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "MAD", + "namePlural": "Moroccan dirhams" + }, + { + "name": "Moldovan Leu", + "symbol": "MDL", + "symbolNative": "MDL", + "decimalDigits": 2, + "rounding": 0, + "code": "MDL", + "namePlural": "Moldovan lei" + }, + { + "name": "Malagasy Ariary", + "symbol": "MGA", + "symbolNative": "MGA", + "decimalDigits": 0, + "rounding": 0, + "code": "MGA", + "namePlural": "Malagasy Ariaries" + }, + { + "name": "Macedonian Denar", + "symbol": "MKD", + "symbolNative": "MKD", + "decimalDigits": 2, + "rounding": 0, + "code": "MKD", + "namePlural": "Macedonian denari" + }, + { + "name": "Myanma Kyat", + "symbol": "MMK", + "symbolNative": "K", + "decimalDigits": 0, + "rounding": 0, + "code": "MMK", + "namePlural": "Myanma kyats" + }, + { + "name": "Macanese Pataca", + "symbol": "MOP$", + "symbolNative": "MOP$", + "decimalDigits": 2, + "rounding": 0, + "code": "MOP", + "namePlural": "Macanese patacas" + }, + { + "name": "Mauritian Rupee", + "symbol": "MURs", + "symbolNative": "MURs", + "decimalDigits": 0, + "rounding": 0, + "code": "MUR", + "namePlural": "Mauritian rupees" + }, + { + "name": "Mexican Peso", + "symbol": "MX$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "MXN", + "namePlural": "Mexican pesos" + }, + { + "name": "Malaysian Ringgit", + "symbol": "RM", + "symbolNative": "RM", + "decimalDigits": 2, + "rounding": 0, + "code": "MYR", + "namePlural": "Malaysian ringgits" + }, + { + "name": "Mozambican Metical", + "symbol": "MTn", + "symbolNative": "MTn", + "decimalDigits": 2, + "rounding": 0, + "code": "MZN", + "namePlural": "Mozambican meticals" + }, + { + "name": "Namibian Dollar", + "symbol": "N$", + "symbolNative": "N$", + "decimalDigits": 2, + "rounding": 0, + "code": "NAD", + "namePlural": "Namibian dollars" + }, + { + "name": "Nigerian Naira", + "symbol": "₦", + "symbolNative": "₦", + "decimalDigits": 2, + "rounding": 0, + "code": "NGN", + "namePlural": "Nigerian nairas" + }, + { + "name": "Nicaraguan Córdoba", + "symbol": "C$", + "symbolNative": "C$", + "decimalDigits": 2, + "rounding": 0, + "code": "NIO", + "namePlural": "Nicaraguan córdobas" + }, + { + "name": "Norwegian Krone", + "symbol": "Nkr", + "symbolNative": "kr", + "decimalDigits": 2, + "rounding": 0, + "code": "NOK", + "namePlural": "Norwegian kroner" + }, + { + "name": "Nepalese Rupee", + "symbol": "NPRs", + "symbolNative": "नेरू", + "decimalDigits": 2, + "rounding": 0, + "code": "NPR", + "namePlural": "Nepalese rupees" + }, + { + "name": "New Zealand Dollar", + "symbol": "NZ$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "NZD", + "namePlural": "New Zealand dollars" + }, + { + "name": "Omani Rial", + "symbol": "OMR", + "symbolNative": "ر.ع.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "OMR", + "namePlural": "Omani rials" + }, + { + "name": "Panamanian Balboa", + "symbol": "B/.", + "symbolNative": "B/.", + "decimalDigits": 2, + "rounding": 0, + "code": "PAB", + "namePlural": "Panamanian balboas" + }, + { + "name": "Peruvian Nuevo Sol", + "symbol": "S/.", + "symbolNative": "S/.", + "decimalDigits": 2, + "rounding": 0, + "code": "PEN", + "namePlural": "Peruvian nuevos soles" + }, + { + "name": "Philippine Peso", + "symbol": "₱", + "symbolNative": "₱", + "decimalDigits": 2, + "rounding": 0, + "code": "PHP", + "namePlural": "Philippine pesos" + }, + { + "name": "Pakistani Rupee", + "symbol": "PKRs", + "symbolNative": "₨", + "decimalDigits": 0, + "rounding": 0, + "code": "PKR", + "namePlural": "Pakistani rupees" + }, + { + "name": "Polish Zloty", + "symbol": "zł", + "symbolNative": "zł", + "decimalDigits": 2, + "rounding": 0, + "code": "PLN", + "namePlural": "Polish zlotys" + }, + { + "name": "Paraguayan Guarani", + "symbol": "₲", + "symbolNative": "₲", + "decimalDigits": 0, + "rounding": 0, + "code": "PYG", + "namePlural": "Paraguayan guaranis" + }, + { + "name": "Qatari Rial", + "symbol": "QR", + "symbolNative": "ر.ق.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "QAR", + "namePlural": "Qatari rials" + }, + { + "name": "Romanian Leu", + "symbol": "RON", + "symbolNative": "RON", + "decimalDigits": 2, + "rounding": 0, + "code": "RON", + "namePlural": "Romanian lei" + }, + { + "name": "Serbian Dinar", + "symbol": "din.", + "symbolNative": "дин.", + "decimalDigits": 0, + "rounding": 0, + "code": "RSD", + "namePlural": "Serbian dinars" + }, + { + "name": "Russian Ruble", + "symbol": "RUB", + "symbolNative": "₽.", + "decimalDigits": 2, + "rounding": 0, + "code": "RUB", + "namePlural": "Russian rubles" + }, + { + "name": "Rwandan Franc", + "symbol": "RWF", + "symbolNative": "FR", + "decimalDigits": 0, + "rounding": 0, + "code": "RWF", + "namePlural": "Rwandan francs" + }, + { + "name": "Saudi Riyal", + "symbol": "SR", + "symbolNative": "ر.س.‏", + "decimalDigits": 2, + "rounding": 0, + "code": "SAR", + "namePlural": "Saudi riyals" + }, + { + "name": "Sudanese Pound", + "symbol": "SDG", + "symbolNative": "SDG", + "decimalDigits": 2, + "rounding": 0, + "code": "SDG", + "namePlural": "Sudanese pounds" + }, + { + "name": "Swedish Krona", + "symbol": "Skr", + "symbolNative": "kr", + "decimalDigits": 2, + "rounding": 0, + "code": "SEK", + "namePlural": "Swedish kronor" + }, + { + "name": "Singapore Dollar", + "symbol": "S$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "SGD", + "namePlural": "Singapore dollars" + }, + { + "name": "Somali Shilling", + "symbol": "Ssh", + "symbolNative": "Ssh", + "decimalDigits": 0, + "rounding": 0, + "code": "SOS", + "namePlural": "Somali shillings" + }, + { + "name": "Syrian Pound", + "symbol": "SY£", + "symbolNative": "ل.س.‏", + "decimalDigits": 0, + "rounding": 0, + "code": "SYP", + "namePlural": "Syrian pounds" + }, + { + "name": "Thai Baht", + "symbol": "฿", + "symbolNative": "฿", + "decimalDigits": 2, + "rounding": 0, + "code": "THB", + "namePlural": "Thai baht" + }, + { + "name": "Tunisian Dinar", + "symbol": "DT", + "symbolNative": "د.ت.‏", + "decimalDigits": 3, + "rounding": 0, + "code": "TND", + "namePlural": "Tunisian dinars" + }, + { + "name": "Tongan Paʻanga", + "symbol": "T$", + "symbolNative": "T$", + "decimalDigits": 2, + "rounding": 0, + "code": "TOP", + "namePlural": "Tongan paʻanga" + }, + { + "name": "Turkish Lira", + "symbol": "TL", + "symbolNative": "TL", + "decimalDigits": 2, + "rounding": 0, + "code": "TRY", + "namePlural": "Turkish Lira" + }, + { + "name": "Trinidad and Tobago Dollar", + "symbol": "TT$", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "TTD", + "namePlural": "Trinidad and Tobago dollars" + }, + { + "name": "New Taiwan Dollar", + "symbol": "NT$", + "symbolNative": "NT$", + "decimalDigits": 2, + "rounding": 0, + "code": "TWD", + "namePlural": "New Taiwan dollars" + }, + { + "name": "Tanzanian Shilling", + "symbol": "TSh", + "symbolNative": "TSh", + "decimalDigits": 0, + "rounding": 0, + "code": "TZS", + "namePlural": "Tanzanian shillings" + }, + { + "name": "Ukrainian Hryvnia", + "symbol": "₴", + "symbolNative": "₴", + "decimalDigits": 2, + "rounding": 0, + "code": "UAH", + "namePlural": "Ukrainian hryvnias" + }, + { + "name": "Ugandan Shilling", + "symbol": "USh", + "symbolNative": "USh", + "decimalDigits": 0, + "rounding": 0, + "code": "UGX", + "namePlural": "Ugandan shillings" + }, + { + "name": "Uruguayan Peso", + "symbol": "$U", + "symbolNative": "$", + "decimalDigits": 2, + "rounding": 0, + "code": "UYU", + "namePlural": "Uruguayan pesos" + }, + { + "name": "Uzbekistan Som", + "symbol": "UZS", + "symbolNative": "UZS", + "decimalDigits": 0, + "rounding": 0, + "code": "UZS", + "namePlural": "Uzbekistan som" + }, + { + "name": "Venezuelan Bolívar", + "symbol": "Bs.F.", + "symbolNative": "Bs.F.", + "decimalDigits": 2, + "rounding": 0, + "code": "VEF", + "namePlural": "Venezuelan bolívars" + }, + { + "name": "Vietnamese Dong", + "symbol": "₫", + "symbolNative": "₫", + "decimalDigits": 0, + "rounding": 0, + "code": "VND", + "namePlural": "Vietnamese dong" + }, + { + "name": "CFA Franc BEAC", + "symbol": "FCFA", + "symbolNative": "FCFA", + "decimalDigits": 0, + "rounding": 0, + "code": "XAF", + "namePlural": "CFA francs BEAC" + }, + { + "name": "CFA Franc BCEAO", + "symbol": "CFA", + "symbolNative": "CFA", + "decimalDigits": 0, + "rounding": 0, + "code": "XOF", + "namePlural": "CFA francs BCEAO" + }, + { + "name": "Yemeni Rial", + "symbol": "YR", + "symbolNative": "ر.ي.‏", + "decimalDigits": 0, + "rounding": 0, + "code": "YER", + "namePlural": "Yemeni rials" + }, + { + "name": "South African Rand", + "symbol": "R", + "symbolNative": "R", + "decimalDigits": 2, + "rounding": 0, + "code": "ZAR", + "namePlural": "South African rand" + }, + { + "name": "Zambian Kwacha", + "symbol": "ZK", + "symbolNative": "ZK", + "decimalDigits": 0, + "rounding": 0, + "code": "ZMK", + "namePlural": "Zambian kwachas" + }, + { + "name": "Zimbabwean Dollar", + "symbol": "ZWL$", + "symbolNative": "ZWL$", + "decimalDigits": 0, + "rounding": 0, + "code": "ZWL", + "namePlural": "Zimbabwean Dollar" + } +] diff --git a/packages/currency/src/index.ts b/packages/currency/src/index.ts new file mode 100644 index 00000000..180ead69 --- /dev/null +++ b/packages/currency/src/index.ts @@ -0,0 +1,3 @@ +import currencies from './currencies.json' + +export { currencies } diff --git a/packages/validation/src/transaction.zod.ts b/packages/validation/src/transaction.zod.ts index da0cb0a9..526559ec 100644 --- a/packages/validation/src/transaction.zod.ts +++ b/packages/validation/src/transaction.zod.ts @@ -1,7 +1,7 @@ import { z } from 'zod' export const zCreateTransaction = z.object({ - date: z.date(), + date: z.date({ coerce: true }), amount: z.number(), currency: z.string(), note: z.string().optional(), @@ -11,7 +11,7 @@ export const zCreateTransaction = z.object({ export type CreateTransaction = z.infer export const zUpdateTransaction = z.object({ - date: z.date().optional(), + date: z.date({ coerce: true }).optional(), amount: z.number().optional(), currency: z.string().optional(), note: z.string().optional(), diff --git a/packages/validation/src/wallet.zod.ts b/packages/validation/src/wallet.zod.ts index dd5d1a66..ccf30433 100644 --- a/packages/validation/src/wallet.zod.ts +++ b/packages/validation/src/wallet.zod.ts @@ -17,3 +17,8 @@ export const zUpdateWallet = z.object({ preferredCurrency: z.string().optional(), }) export type UpdateWallet = z.infer + +export const zAccountFormValues = zCreateWallet.extend({ + balance: z.number({ coerce: true }).positive().optional(), +}) +export type AccountFormValues = z.infer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ff21e9c..90787072 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: '@6pm/api': specifier: workspace:^ version: link:../api + '@6pm/currency': + specifier: workspace:^ + version: link:../../packages/currency '@6pm/validation': specifier: workspace:^ version: link:../../packages/validation @@ -111,6 +114,9 @@ importers: '@formatjs/intl-pluralrules': specifier: ^5.2.14 version: 5.2.14 + '@gorhom/bottom-sheet': + specifier: ^4.6.3 + version: 4.6.3(@types/react@18.2.79)(react-native-gesture-handler@2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) '@hookform/resolvers': specifier: ^3.6.0 version: 3.6.0(react-hook-form@7.52.0(react@18.3.1)) @@ -120,6 +126,9 @@ importers: '@lingui/react': specifier: ^4.11.1 version: 4.11.1(react@18.3.1) + '@lukemorales/query-key-factory': + specifier: ^1.3.4 + version: 1.3.4(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1)) '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1439,6 +1448,27 @@ packages: '@formkit/auto-animate@0.8.2': resolution: {integrity: sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==} + '@gorhom/bottom-sheet@4.6.3': + resolution: {integrity: sha512-fSuSfbtoKsjmSeyz+tG2C0GtcEL7PS63iEXI23c9M+HeCT1IFK6ffmIa2pqyqB43L1jtkR+BWkpZwqXnN4H8xA==} + peerDependencies: + '@types/react': '*' + '@types/react-native': '*' + react: '*' + react-native: '*' + react-native-gesture-handler: '>=1.10.1' + react-native-reanimated: '>=2.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-native': + optional: true + + '@gorhom/portal@1.0.14': + resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==} + peerDependencies: + react: '*' + react-native: '*' + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: @@ -1621,6 +1651,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@lukemorales/query-key-factory@1.3.4': + resolution: {integrity: sha512-A3frRDdkmaNNQi6mxIshsDk4chRXWoXa05US8fBo4kci/H+lVmujS6QrwQLLGIkNIRFGjMqp2uKjC4XsLdydRw==} + engines: {node: '>=14'} + peerDependencies: + '@tanstack/query-core': '>= 4.0.0' + '@tanstack/react-query': '>= 4.0.0' + '@messageformat/parser@5.1.0': resolution: {integrity: sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==} @@ -7927,6 +7964,23 @@ snapshots: '@formkit/auto-animate@0.8.2': {} + '@gorhom/bottom-sheet@4.6.3(@types/react@18.2.79)(react-native-gesture-handler@2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)': + dependencies: + '@gorhom/portal': 1.0.14(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1) + react-native-gesture-handler: 2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.79 + + '@gorhom/portal@1.0.14(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)': + dependencies: + nanoid: 3.3.7 + react: 18.3.1 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1) + '@graphql-typed-document-node/core@3.2.0(graphql@15.8.0)': dependencies: graphql: 15.8.0 @@ -8279,6 +8333,11 @@ snapshots: '@lingui/core': 4.11.1 react: 18.3.1 + '@lukemorales/query-key-factory@1.3.4(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))': + dependencies: + '@tanstack/query-core': 5.45.0 + '@tanstack/react-query': 5.45.1(react@18.3.1) + '@messageformat/parser@5.1.0': dependencies: moo: 0.5.2