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): allow input negative wallet balance and float numbers #225

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
2 changes: 1 addition & 1 deletion apps/api/v1/services/budget.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export async function createBudget({
create: {
id: period.id,
type: period.type,
amount: period.amount,
amount: period.amount ?? 0,
startDate: period.startDate ?? periodConfig.startDate,
endDate: period.endDate ?? periodConfig.endDate,
},
Expand Down
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
Loading