Skip to content
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
32 changes: 17 additions & 15 deletions app/bills/pay/[billerId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ interface PageProps {
}

export default function BillerPaymentPage({ params }: PageProps) {
const { billerId } = use(params)
const schema = BILLER_SCHEMAS[billerId]
const { billerId } = use(params)
const schema = BILLER_SCHEMAS[billerId]

if (!schema) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="text-center space-y-4">
<h1 className="text-2xl font-bold">Biller Not Found</h1>
<p className="text-muted-foreground">The biller you are looking for does not exist or is not supported yet.</p>
<Button asChild>
<Link href="/bills">Back to Bills</Link>
</Button>
</div>
</div>
)
}
if (!schema) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="text-center space-y-4">
<h1 className="text-2xl font-bold">Biller Not Found</h1>
<p className="text-muted-foreground">
The biller you are looking for does not exist or is not supported yet.
</p>
<Button asChild>
<Link href="/bills">Back to Bills</Link>
</Button>
</div>
</div>
)
}

if (!schema) {
return (
Expand Down
115 changes: 54 additions & 61 deletions components/bills/fee-breakdown.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,66 @@
'use client'

import { Info } from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'

interface FeeBreakdownProps {
amount: number
baseFee: number
percentageFee: number
currency?: string
amount: number
baseFee: number
percentageFee: number
currency?: string
}

export function FeeBreakdown({
amount,
baseFee,
percentageFee,
currency = '₦',
amount,
baseFee,
percentageFee,
currency = '₦',
}: FeeBreakdownProps) {
const calcPercentageFee = amount * percentageFee
const totalFee = baseFee + calcPercentageFee
const totalAmount = amount + totalFee
const calcPercentageFee = amount * percentageFee
const totalFee = baseFee + calcPercentageFee
const totalAmount = amount + totalFee

return (
<div className="rounded-2xl border border-border bg-muted/30 p-4 space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">
{currency}
{amount.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<div className="flex items-center gap-1.5 text-muted-foreground text-xs">
<span>Processing Fee</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button type="button">
<Info className="w-3.5 h-3.5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>
Flat fee: {currency}
{baseFee}
</p>
{percentageFee > 0 && (
<p>Processing: {(percentageFee * 100).toFixed(1)}%</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<span className="text-muted-foreground">
{currency}
{totalFee.toLocaleString()}
</span>
</div>
<div className="pt-2 mt-2 border-t border-border flex justify-between items-center">
<span className="font-semibold text-foreground">Total to Pay</span>
<span className="text-lg font-bold text-primary">
{currency}
{totalAmount.toLocaleString()}
</span>
</div>
return (
<div className="rounded-2xl border border-border bg-muted/30 p-4 space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">
{currency}
{amount.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<div className="flex items-center gap-1.5 text-muted-foreground text-xs">
<span>Processing Fee</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button type="button">
<Info className="w-3.5 h-3.5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>
Flat fee: {currency}
{baseFee}
</p>
{percentageFee > 0 && <p>Processing: {(percentageFee * 100).toFixed(1)}%</p>}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)
<span className="text-muted-foreground">
{currency}
{totalFee.toLocaleString()}
</span>
</div>
<div className="pt-2 mt-2 border-t border-border flex justify-between items-center">
<span className="font-semibold text-foreground">Total to Pay</span>
<span className="text-lg font-bold text-primary">
{currency}
{totalAmount.toLocaleString()}
</span>
</div>
</div>
)
}
100 changes: 27 additions & 73 deletions components/bills/payment-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,16 @@ export function PaymentForm({ schema }: PaymentFormProps) {
const [isProcessing, setIsProcessing] = useState(false)
const [showSchedule, setShowSchedule] = useState(false)

// Generate dynamic Zod schema
// 1. Generate dynamic Zod schema
const formSchemaObject: any = {}
schema.fields.forEach((field) => {
let validator: any = 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)
}

if (field.validation.minLength && field.type === 'number') {
const minVal = field.validation.minLength
validator = validator.refine(
(val: string) => {
const num = parseFloat(val)
return !isNaN(num) && num >= minVal
},
field.validation.message || `Minimum value is ${minVal}`
)
}

formSchemaObject[field.name] = validator
})

Expand All @@ -75,27 +61,22 @@ export function PaymentForm({ schema }: PaymentFormProps) {
mode: 'onChange',
})

// Mock real-time account validation
const accountValue = watch(schema.fields[0].name as any)
useEffect(() => {
if (accountValue && accountValue.length >= 10 && !errors[schema.fields[0].name]) {
const delayDebounceFn = setTimeout(() => {
validateAccount()
}, 1000)
return () => clearTimeout(delayDebounceFn)
} else {
setValidatedAccount(null)
}
}, [accountValue, errors[schema.fields[0].name]])

const validateAccount = async () => {
setIsValidating(true)
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500))
setIsValidating(false)
// 2. Define parsedAmount (was missing)
const amountField = schema.fields.find((f) => f.name === 'amount' || f.type === 'number')
const amountValue = watch(amountField?.name as any)
const parsedAmount = parseFloat(amountValue || '0')

// Mock real-time account validation
// 3. Logic: Account Validation
const accountValue = watch(schema.fields[0].name as any)

const validateAccount = 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(mockNames[Math.floor(Math.random() * mockNames.length)])
}

useEffect(() => {
if (accountValue && accountValue.length >= 10 && !errors[schema.fields[0].name]) {
const delayDebounceFn = setTimeout(() => {
Expand All @@ -107,26 +88,10 @@ export function PaymentForm({ schema }: PaymentFormProps) {
}
}, [accountValue, errors[schema.fields[0].name]])

const validateAccount = async (_value: string) => {
setIsValidating(true)
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500))
setIsValidating(false)
const onSubmit = async (_data: FormValues) => {
setIsProcessing(true)
// Simulate payment processing
await new Promise((resolve) => setTimeout(resolve, 3000))

// Random mock name
const mockNames = ['John Doe', 'Sarah Williams', 'Emeka Azikiwe', 'Kofi Mensah', 'Jane Smith']
setValidatedAccount(mockNames[Math.floor(Math.random() * mockNames.length)])
}

const onSubmit = async (_data: FormValues) => {
// 4. Logic: Form Submission
const onSubmit = async (data: FormValues) => {
setIsProcessing(true)
// Simulate payment processing
await new Promise((resolve) => setTimeout(resolve, 3000))

setIsProcessing(false)
toast.success('Payment Successful!', {
description: `Your payment to ${schema.name} has been processed.`,
Expand All @@ -143,7 +108,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
</Label>

{field.type === 'select' ? (
<Select onValueChange={(val) => setValue(field.name as any, val)}>
<Select onValueChange={(val: any) => setValue(field.name as any, val)}>
<SelectTrigger className="h-12 rounded-2xl bg-muted/30 focus:ring-primary">
<SelectValue placeholder={field.placeholder || `Select ${field.label}`} />
</SelectTrigger>
Expand Down Expand Up @@ -176,7 +141,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-xs flex items-center gap-1.5 text-success font-medium bg-success/10 p-2 rounded-xl"
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}
Expand All @@ -199,13 +164,10 @@ export function PaymentForm({ schema }: PaymentFormProps) {

<div className="space-y-4">
<div className="flex items-center space-x-2 bg-muted/20 p-4 rounded-2xl border border-border/50">
<Checkbox
id="saveDetails"
className="rounded-lg border-primary data-[state=checked]:bg-primary"
/>
<Checkbox id="saveDetails" className="rounded-lg" />
<label
htmlFor="saveDetails"
className="text-sm font-medium leading-none cursor-pointer text-muted-foreground"
className="text-sm font-medium cursor-pointer text-muted-foreground"
>
Save details for future payments
</label>
Expand All @@ -220,8 +182,7 @@ export function PaymentForm({ schema }: PaymentFormProps) {
<Checkbox
id="schedule"
checked={showSchedule}
onCheckedChange={(checked) => setShowSchedule(!!checked)}
className="rounded-lg border-primary data-[state=checked]:bg-primary"
onCheckedChange={(checked: any) => setShowSchedule(!!checked)}
/>
</div>

Expand All @@ -234,9 +195,6 @@ export function PaymentForm({ schema }: PaymentFormProps) {
className="overflow-hidden pt-2"
>
<Input type="date" className="h-11 rounded-xl bg-card border-border" />
<p className="text-[10px] text-muted-foreground mt-2">
Payment will be automatically processed on the selected date.
</p>
</motion.div>
)}
</AnimatePresence>
Expand All @@ -259,24 +217,20 @@ export function PaymentForm({ schema }: PaymentFormProps) {
disabled={
!isValid || isProcessing || (schema.fields[0].validation.required && !validatedAccount)
}
className="w-full h-14 rounded-2xl text-lg font-semibold shimmer-btn shadow-lg shadow-primary/20 transition-all active:scale-[0.98]"
className="w-full h-14 rounded-2xl text-lg font-semibold"
>
{isProcessing ? (
<div className="flex items-center gap-2 text-primary-foreground">
<div className="flex items-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span>Processing...</span>
</div>
) : (
<div className="flex items-center gap-2 text-primary-foreground">
<div className="flex items-center gap-2">
<span>Pay Now</span>
<ChevronRight className="w-5 h-5" />
</div>
)}
</Button>

<p className="text-[10px] text-center text-muted-foreground px-6">
By clicking &quot;Pay Now&quot;, you agree to our Terms of Service and acknowledge that this transaction is final.
</p>
</form>
)
</form>
)
}
Loading
Loading