Skip to content

Commit

Permalink
feat(mobile): allow input negative wallet balance and float numbers
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 committed Aug 14, 2024
1 parent b708d49 commit 49b7843
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 47 deletions.
69 changes: 40 additions & 29 deletions apps/mobile/app/(app)/wallet/[walletId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { AccountForm } from '@/components/wallet/account-form'
import { deleteWallet, updateWallet } from '@/mutations/wallet'
import { transactionQueries } from '@/queries/transaction'
import { useWallets, walletQueries } from '@/queries/wallet'
import { WalletBalanceState } from '@6pm/validation'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router'
import { Trash2Icon } from 'lucide-react-native'
import { useEffect } from 'react'
import { Alert, ScrollView } from 'react-native'
import { Alert, ScrollView, View } from 'react-native'

export default function EditAccountScreen() {
const { walletId } = useLocalSearchParams()
const { data: walletAccounts } = useWallets()
const { sideOffset, ...rootProps } = useModalPortalRoot()
const { i18n } = useLingui()
const queryClient = useQueryClient()
const router = useRouter()
Expand Down Expand Up @@ -94,33 +97,41 @@ export default function EditAccountScreen() {
}

return (
<ScrollView
className="flex-1 bg-card"
contentContainerClassName="gap-4 p-6"
automaticallyAdjustKeyboardInsets
keyboardShouldPersistTaps="handled"
>
<AccountForm
onSubmit={({ balance, ...data }) => {
const adjustedBalance =
(balance ?? 0) - ((walletAccount.balance as number) ?? 0)
mutateUpdate({
id: walletId as string,
data: {
...data,
balance: adjustedBalance,
},
})
}}
defaultValues={{
name: walletAccount.name,
preferredCurrency: walletAccount.preferredCurrency,
balance: walletAccount.balance,
icon: walletAccount.icon ?? 'CreditCard',
description: walletAccount.description ?? '',
lastDigits: walletAccount.lastDigits ?? '',
}}
/>
</ScrollView>
<View className="flex-1 bg-card" {...rootProps}>
<ScrollView
className="flex-1 bg-card"
contentContainerClassName="gap-4 p-6"
automaticallyAdjustKeyboardInsets
keyboardShouldPersistTaps="handled"
>
<AccountForm
onSubmit={({ balance, ...data }) => {
const statedBalance =
data.balanceState === WalletBalanceState.Positive
? balance
: (balance ?? 0) * -1
const adjustedBalance =
(statedBalance ?? 0) - ((walletAccount.balance as number) ?? 0)
mutateUpdate({
id: walletId as string,
data: {
...data,
balance: adjustedBalance,
},
})
}}
defaultValues={{
name: walletAccount.name,
preferredCurrency: walletAccount.preferredCurrency,
balance: walletAccount.balance,
icon: walletAccount.icon ?? 'CreditCard',
description: walletAccount.description ?? '',
lastDigits: walletAccount.lastDigits ?? '',
}}
sideOffset={sideOffset}
/>
</ScrollView>
<PortalHost name="account-form" />
</View>
)
}
36 changes: 27 additions & 9 deletions apps/mobile/app/(app)/wallet/new-account.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { AccountForm } from '@/components/wallet/account-form'
import { createWallet } from '@/mutations/wallet'
import { walletQueries } from '@/queries/wallet'
import { WalletBalanceState } from '@6pm/validation'
import { PortalHost, useModalPortalRoot } from '@rn-primitives/portal'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'expo-router'
import { Alert, ScrollView } from 'react-native'
import { Alert, ScrollView, View } from 'react-native'

export default function NewAccountScreen() {
const queryClient = useQueryClient()
const { sideOffset, ...rootProps } = useModalPortalRoot()
const router = useRouter()
const { mutateAsync } = useMutation({
mutationFn: createWallet,
Expand Down Expand Up @@ -50,13 +53,28 @@ export default function NewAccountScreen() {
})

return (
<ScrollView
className="flex-1 bg-card"
contentContainerClassName="gap-4 p-6"
automaticallyAdjustKeyboardInsets
keyboardShouldPersistTaps="handled"
>
<AccountForm onSubmit={mutateAsync} />
</ScrollView>
<View className="flex-1 bg-card" {...rootProps}>
<ScrollView
className="flex-1 bg-card"
contentContainerClassName="gap-4 p-6"
automaticallyAdjustKeyboardInsets
keyboardShouldPersistTaps="handled"
>
<AccountForm
onSubmit={({ balance, ...data }) => {
const statedBalance =
data.balanceState === WalletBalanceState.Positive
? balance
: (balance ?? 0) * -1
mutateAsync({
...data,
balance: statedBalance,
})
}}
sideOffset={sideOffset}
/>
</ScrollView>
<PortalHost name="account-form" />
</View>
)
}
2 changes: 1 addition & 1 deletion apps/mobile/components/budget/budget-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const BudgetForm = ({
label={t(i18n)`Target`}
placeholder={t(i18n)`0.00`}
className="!pl-[62px]"
keyboardType="number-pad"
keyboardType="numeric"
leftSection={
<Controller
name="preferredCurrency"
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/components/ui/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from '@rn-primitives/collapsible'

const Collapsible = CollapsiblePrimitive.Root

const CollapsibleTrigger = CollapsiblePrimitive.Trigger

const CollapsibleContent = CollapsiblePrimitive.Content

export { Collapsible, CollapsibleTrigger, CollapsibleContent }
73 changes: 69 additions & 4 deletions apps/mobile/components/wallet/account-form.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
import { useDefaultCurrency } from '@/stores/user-settings/hooks'
import { type WalletFormValues, zWalletFormValues } from '@6pm/validation'
import {
WalletBalanceState,
type WalletFormValues,
zWalletFormValues,
} from '@6pm/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useRef } from 'react'
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react-native'
import { useRef, useState } from 'react'
import { Controller, FormProvider, useForm } from 'react-hook-form'
import { View } from 'react-native'
import type { TextInput } from 'react-native'
import { CurrencyField } from '../form-fields/currency-field'
import { InputField } from '../form-fields/input-field'
import { SubmitButton } from '../form-fields/submit-button'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '../ui/collapsible'
import { Label } from '../ui/label'
import { Text } from '../ui/text'
import { SelectAccountIconField } from './select-account-icon-field'
import { SelectBalanceStateField } from './select-balance-state-field'

type AccountFormProps = {
onSubmit: (data: WalletFormValues) => void
defaultValues?: WalletFormValues
sideOffset?: number
}

export const AccountForm = ({ onSubmit, defaultValues }: AccountFormProps) => {
export const AccountForm = ({
onSubmit,
defaultValues,
sideOffset,
}: AccountFormProps) => {
const { i18n } = useLingui()
const nameInputRef = useRef<TextInput>(null)
const balanceInputRef = useRef<TextInput>(null)
const defaultCurrency = useDefaultCurrency()
const [isExpanded, setIsExpanded] = useState(
(defaultValues?.balance || 0) < 0,
)

const accountForm = useForm<WalletFormValues>({
resolver: zodResolver(zWalletFormValues),
defaultValues: {
name: '',
preferredCurrency: defaultCurrency,
icon: 'CreditCard',
balanceState:
Number.isNaN(defaultValues?.balance) ||
(defaultValues?.balance || 0) >= 0
? WalletBalanceState.Positive
: WalletBalanceState.Negative,
...defaultValues,
balance: Math.abs(defaultValues?.balance || 0),
},
})

Expand Down Expand Up @@ -59,7 +85,7 @@ export const AccountForm = ({ onSubmit, defaultValues }: AccountFormProps) => {
label={t(i18n)`Balance`}
placeholder={t(i18n)`0.00`}
className="!pl-[62px]"
keyboardType="number-pad"
keyboardType="numeric"
leftSection={
<Controller
name="preferredCurrency"
Expand All @@ -76,6 +102,45 @@ export const AccountForm = ({ onSubmit, defaultValues }: AccountFormProps) => {
/>
}
/>
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger>
<View className="mx-auto mt-2 mb-4 flex-row items-center gap-2">
<Text className="font-medium">{t(i18n)`Advanced`}</Text>
{isExpanded ? (
<ChevronUpIcon className="h-5 w-5 text-primary" />
) : (
<ChevronDownIcon className="h-5 w-5 text-primary" />
)}
</View>
</CollapsibleTrigger>
<CollapsibleContent>
<Controller
name="balanceState"
control={accountForm.control}
render={({ field: { onChange, value, disabled } }) => (
<View>
<Label nativeID={`label-state`}>{t(
i18n,
)`Current state`}</Label>
<Label
className="!text-xs mb-1.5 font-normal text-muted-foreground"
nativeID={`label-state`}
>{t(
i18n,
)`Negative if your content balance is under zero`}</Label>
<SelectBalanceStateField
value={value || WalletBalanceState.Positive}
sideOffset={sideOffset}
onSelect={(selected) => {
onChange(selected)
}}
disabled={disabled}
/>
</View>
)}
/>
</CollapsibleContent>
</Collapsible>
<SubmitButton
onPress={accountForm.handleSubmit(onSubmit)}
disabled={accountForm.formState.isLoading}
Expand Down
86 changes: 86 additions & 0 deletions apps/mobile/components/wallet/select-balance-state-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { WalletBalanceState } from '@6pm/validation'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMemo } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select'

type SelectBalanceStateFieldProps = {
value: string
onSelect: (type?: string) => void
sideOffset?: number
disabled?: boolean
}

export function SelectBalanceStateField({
value,
onSelect,
sideOffset,
disabled,
}: SelectBalanceStateFieldProps) {
const { i18n } = useLingui()
const insets = useSafeAreaInsets()
const contentInsets = {
top: insets.top,
bottom: insets.bottom + Math.abs(sideOffset || 0),
left: 21,
right: 21,
}

const options = useMemo(
() => [
{
value: WalletBalanceState.Positive,
label: t(i18n)`Positive`,
},
{
value: WalletBalanceState.Negative,
label: t(i18n)`Negative`,
},
],
[i18n],
)

return (
<Select
defaultValue={options[0]}
value={options.find((option) => option.value === value)}
onValueChange={(selected) => onSelect(selected?.value)}
>
<SelectTrigger disabled={disabled}>
<SelectValue
className="font-sans text-foreground"
placeholder={t(i18n)`Select balance state`}
>
{value}
</SelectValue>
</SelectTrigger>
<SelectContent
sideOffset={(sideOffset || 0) + 6}
insets={contentInsets}
alignOffset={10}
portalHost="account-form"
className="w-full"
>
<SelectGroup className="px-1">
{options.map((option) => (
<SelectItem
key={option.value}
value={option.value}
label={option.label}
>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@react-native-community/datetimepicker": "8.0.1",
"@react-native-community/netinfo": "11.3.1",
"@react-navigation/native": "^6.0.2",
"@rn-primitives/collapsible": "^1.0.3",
"@rn-primitives/dropdown-menu": "^1.0.3",
"@rn-primitives/portal": "^1.0.3",
"@rn-primitives/progress": "^1.0.3",
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/queries/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getHonoClient } from '@/lib/client'
import { WalletAccountWithBalanceSchema } from '@6pm/validation'
import { createQueryKeys } from '@lukemorales/query-key-factory'
import { useQuery } from '@tanstack/react-query'
import { orderBy } from 'lodash-es'

export const walletQueries = createQueryKeys('wallet', {
list: () => ({
Expand All @@ -16,7 +17,7 @@ export const walletQueries = createQueryKeys('wallet', {
const result = rawResult.map((item) =>
WalletAccountWithBalanceSchema.parse(item),
)
return result
return orderBy(result, 'name')
},
}),
})
Expand Down
Loading

0 comments on commit 49b7843

Please sign in to comment.