Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ jobs:
repo: context.repo.repo,
body: '🧹 Preview deployment deleted.'
});

81 changes: 31 additions & 50 deletions components/bills/payment-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect } from 'react'
import { useForm, useWatch } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
Expand All @@ -22,31 +22,29 @@ import { toast } from 'sonner'
import { motion, AnimatePresence } from 'framer-motion'
import { cn } from '@/lib/utils'
import { Checkbox } from '@/components/ui/checkbox'
import type { CheckedState } from '@radix-ui/react-checkbox'

interface PaymentFormProps {
schema: BillerSchema
}

export function PaymentForm({ schema }: PaymentFormProps) {
const [isValidating, setIsValidating] = useState(false)
const [validatedAccount, setValidatedAccount] = useState<{
name: string
accountValue: string
} | null>(null)
const [validatedAccount, setValidatedAccount] = useState<string | null>(null)
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>('card')
const [isProcessing, setIsProcessing] = useState(false)
const [showSchedule, setShowSchedule] = useState(false)

// 1. Generate dynamic Zod schema
const formSchemaObject: Record<string, z.ZodString> = {}
const formSchemaObject: Record<string, z.ZodTypeAny> = {}
schema.fields.forEach((field) => {
let validator = z.string()
let validator: z.ZodString | z.ZodNumber = z.string()
if (field.validation.required) {
validator = validator.min(1, field.validation.message || `${field.label} is required`)
}
if (field.validation.pattern) {
validator = validator.regex(new RegExp(field.validation.pattern), field.validation.message)
validator = (validator as z.ZodString).regex(
new RegExp(field.validation.pattern),
field.validation.message
)
}
formSchemaObject[field.name] = validator
})
Expand All @@ -67,45 +65,33 @@ export function PaymentForm({ schema }: PaymentFormProps) {

const watchedValues = useWatch({ control })

// 2. Define parsedAmount (was missing)
const amountField = schema.fields.find((f) => f.name === 'amount' || f.type === 'number')
const amountValue = amountField?.name ? watchedValues[amountField.name] : ''
const parsedAmount = parseFloat((amountValue as string) || '0')
const amountValue = watchedValues[amountField?.name as keyof FormValues] as string
const parsedAmount = parseFloat(amountValue || '0') || 0

// 3. Logic: Account Validation
const primaryFieldName = schema.fields[0]?.name || ''
const accountValue = primaryFieldName ? ((watchedValues[primaryFieldName] as string) ?? '') : ''
const accountError = primaryFieldName ? errors[primaryFieldName] : undefined

const validateAccount = useCallback(async (_value: string) => {
setIsValidating(true)
await new Promise((resolve) => setTimeout(resolve, 1500))
setIsValidating(false)
const mockNames = ['John Doe', 'Sarah Williams', 'Emeka Azikiwe', 'Kofi Mensah']
setValidatedAccount({
name: mockNames[Math.floor(Math.random() * mockNames.length)],
accountValue: _value,
})
}, [])
const accountValue = (watchedValues[primaryFieldName as keyof FormValues] as string) || ''

useEffect(() => {
if (accountValue && accountValue.length >= 10 && !accountError) {
const validate = async () => {
setIsValidating(true)
await new Promise((resolve) => setTimeout(resolve, 1500))
setIsValidating(false)
const mockNames = ['John Doe', 'Sarah Williams', 'Emeka Azikiwe', 'Kofi Mensah']
setValidatedAccount(mockNames[Math.floor(Math.random() * mockNames.length)])
}

if (accountValue && accountValue.length >= 10 && !errors[primaryFieldName]) {
const delayDebounceFn = setTimeout(() => {
validateAccount(accountValue)
validate()
}, 1000)
return () => clearTimeout(delayDebounceFn)
} else {
setValidatedAccount(null)
}
}, [accountError, accountValue, validateAccount])

const isAccountValidatedForCurrentInput = Boolean(
validatedAccount &&
validatedAccount.accountValue === accountValue &&
accountValue.length >= 10 &&
!accountError
)
}, [accountValue, errors, primaryFieldName])

// 4. Logic: Form Submission
const onSubmit = async (_data: FormValues) => {
const onSubmit = async (data: FormValues) => {
setIsProcessing(true)
await new Promise((resolve) => setTimeout(resolve, 3000))
setIsProcessing(false)
Expand All @@ -126,10 +112,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
{field.type === 'select' ? (
<Select
onValueChange={(val: string) =>
setValue(field.name, val as FormValues[typeof field.name], {
shouldValidate: true,
shouldDirty: true,
})
setValue(field.name as any, val, { shouldValidate: true })
}
>
<SelectTrigger className="h-12 rounded-2xl bg-muted/30 focus:ring-primary">
Expand All @@ -153,21 +136,21 @@ export function PaymentForm({ schema }: PaymentFormProps) {
'h-12 rounded-2xl bg-muted/30 focus:ring-primary',
isValidating && field.id === schema.fields[0]?.id && 'pr-10'
)}
{...register(field.name)}
{...register(field.name as any)}
/>
{isValidating && field.id === schema.fields[0]?.id && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="w-5 h-5 animate-spin text-primary" />
</div>
)}
{isAccountValidatedForCurrentInput && field.id === schema.fields[0]?.id && (
{validatedAccount && field.id === schema.fields[0]?.id && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-xs flex items-center gap-1.5 text-green-600 font-medium bg-green-50 p-2 rounded-xl"
>
<CheckCircle2 className="w-4 h-4" />
Account Verified: {validatedAccount?.name}
Account Verified: {validatedAccount}
</motion.div>
)}
</div>
Expand Down Expand Up @@ -205,7 +188,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
<Checkbox
id="schedule"
checked={showSchedule}
onCheckedChange={(checked: CheckedState) => setShowSchedule(checked === true)}
onCheckedChange={(checked: boolean) => setShowSchedule(!!checked)}
/>
</div>

Expand Down Expand Up @@ -238,9 +221,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
<Button
type="submit"
disabled={
!isValid ||
isProcessing ||
(schema.fields[0].validation.required && !isAccountValidatedForCurrentInput)
!isValid || isProcessing || (schema.fields[0].validation.required && !validatedAccount)
}
className="w-full h-14 rounded-2xl text-lg font-semibold"
>
Expand Down
Loading