fix(wallet): honor configured quota units for reward transfers (#5808)

* fix(wallet): honor configured quota units for reward transfers

* fix(i18n): localize quota preview descriptions

* fix(settings): normalize invalid quota input
This commit is contained in:
A_Words
2026-07-07 21:40:58 +08:00
committed by GitHub
parent a72e5082e9
commit 90fa6fe6b6
11 changed files with 112 additions and 31 deletions
@@ -34,6 +34,7 @@ import {
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { formatQuota } from '@/lib/format'
import { FormDirtyIndicator } from '../components/form-dirty-indicator'
import { FormNavigationGuard } from '../components/form-navigation-guard'
@@ -64,6 +65,11 @@ const quotaSchema = z.object({
})
type QuotaFormValues = z.infer<typeof quotaSchema>
type QuotaInputValue = number | ''
function formatQuotaInputValue(value: QuotaInputValue): string {
return formatQuota(value === '' ? 0 : value)
}
type QuotaSettingsSectionProps = {
defaultValues: QuotaFormValues
@@ -77,11 +83,10 @@ export function QuotaSettingsSection({
const { t } = useTranslation()
const updateOption = useUpdateOption()
const handleNumberChange =
(onChange: (value: number | string) => void) =>
(onChange: (value: QuotaInputValue) => void) =>
(event: ChangeEvent<HTMLInputElement>) => {
onChange(
event.target.value === '' ? '' : event.currentTarget.valueAsNumber
)
const value = event.currentTarget.valueAsNumber
onChange(Number.isNaN(value) ? '' : value)
}
const { form, handleSubmit, isDirty, isSubmitting } =
@@ -141,7 +146,12 @@ export function QuotaSettingsSection({
/>
</FormControl>
<FormDescription>
{t('Initial quota given to new users')}
{t(
'Initial quota given to new users ({{formattedQuota}})',
{
formattedQuota: formatQuotaInputValue(field.value),
}
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -189,7 +199,12 @@ export function QuotaSettingsSection({
/>
</FormControl>
<FormDescription>
{t('Quota given to users who invite others')}
{t(
'Quota given to users who invite others ({{formattedQuota}})',
{
formattedQuota: formatQuotaInputValue(field.value),
}
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -213,7 +228,9 @@ export function QuotaSettingsSection({
/>
</FormControl>
<FormDescription>
{t('Quota given to invited users')}
{t('Quota given to invited users ({{formattedQuota}})', {
formattedQuota: formatQuotaInputValue(field.value),
})}
</FormDescription>
<FormMessage />
</FormItem>
@@ -24,9 +24,15 @@ import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { formatQuota } from '@/lib/format'
import { QUOTA_PER_DOLLAR } from '../../constants'
import {
formatQuota,
parseQuotaFromDollars,
quotaUnitsToDollars,
} from '@/lib/format'
import {
DEFAULT_CURRENCY_CONFIG,
useSystemConfigStore,
} from '@/stores/system-config-store'
interface TransferDialogProps {
open: boolean
@@ -44,17 +50,34 @@ export function TransferDialog({
transferring,
}: TransferDialogProps) {
const { t } = useTranslation()
const [amount, setAmount] = useState(QUOTA_PER_DOLLAR)
const currencyConfig = useSystemConfigStore(
(state) => state.config.currency
)
const minimumQuota = Math.ceil(
currencyConfig.quotaPerUnit > 0
? currencyConfig.quotaPerUnit
: DEFAULT_CURRENCY_CONFIG.quotaPerUnit
)
const minimumAmount = quotaUnitsToDollars(minimumQuota)
const maximumAmount = quotaUnitsToDollars(availableQuota)
const [amount, setAmount] = useState(minimumAmount)
const transferQuota = parseQuotaFromDollars(amount)
const canTransfer =
Number.isFinite(amount) &&
transferQuota >= minimumQuota &&
transferQuota <= availableQuota
useEffect(() => {
if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setAmount(QUOTA_PER_DOLLAR)
setAmount(minimumAmount)
}
}, [open])
}, [minimumAmount, open])
const handleConfirm = async () => {
const success = await onConfirm(amount)
if (!canTransfer) return
const success = await onConfirm(transferQuota)
if (success) {
onOpenChange(false)
}
@@ -80,7 +103,10 @@ export function TransferDialog({
>
{t('Cancel')}
</Button>
<Button onClick={handleConfirm} disabled={transferring}>
<Button
onClick={handleConfirm}
disabled={transferring || !canTransfer}
>
{transferring && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{t('Transfer')}
</Button>
@@ -109,13 +135,13 @@ export function TransferDialog({
type='number'
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
min={QUOTA_PER_DOLLAR}
max={availableQuota}
step={QUOTA_PER_DOLLAR}
min={minimumAmount}
max={maximumAmount}
step={minimumAmount}
className='font-mono text-lg'
/>
<p className='text-muted-foreground text-xs'>
{t('Minimum:')} {formatQuota(QUOTA_PER_DOLLAR)}
{t('Minimum:')} {formatQuota(minimumQuota)}
</p>
</div>
</div>
-5
View File
@@ -55,11 +55,6 @@ export const PAYMENT_ICON_COLORS = {
[PAYMENT_TYPES.WAFFO_PANCAKE]: '#F97316',
} as const
/**
* Quota conversion rate: 500,000 units = $1
*/
export const QUOTA_PER_DOLLAR = 500000
/**
* Default discount rate (no discount)
*/