mywork / src /components /billing /CoinPurchaseForm.tsx
DeeCeeXxx's picture
Upload 114 files
e9d5b7d verified
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, Controller } from "react-hook-form";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { CoinPurchaseSchema, type CoinPurchaseInput, CoinPurchasePackageEnum, PaymentGatewayEnum, SupportedCurrencyEnum } from "@/lib/schemas";
import type { CoinPackageDetails, CurrencyInfo } from "@/lib/types";
import { useToast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { Loader2, Coins, CreditCard, ShoppingCart, Lock } from "lucide-react";
import { usePaystackPayment, PaystackButton, PaystackConsumer } from 'react-paystack';
import { useFlutterwave, closePaymentModal } from 'flutterwave-react-v3';
import { initiateCoinPurchase, verifyPaymentAndAwardCoins } from "@/lib/actions/billing";
import { getLoggedInUser, type LoggedInUser } from "@/lib/actions/auth";
const paystackPublicKey = process.env.NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY || '';
const flutterwavePublicKey = process.env.NEXT_PUBLIC_FLUTTERWAVE_PUBLIC_KEY || '';
const COIN_BASE_PRICE_NGN = 10; // 1 Coin = 10 NGN
const coinPackages: CoinPackageDetails[] = [
{ id: 'small_50', name: "Small Pack", coins: 50, priceNGN: 50 * COIN_BASE_PRICE_NGN, description: "Get started with 50 coins." },
{ id: 'medium_150', name: "Medium Pack", coins: 150, priceNGN: 150 * COIN_BASE_PRICE_NGN, description: "Most popular: 150 coins." },
{ id: 'large_300', name: "Large Pack", coins: 300, priceNGN: 300 * COIN_BASE_PRICE_NGN, description: "Best value: 300 coins." },
];
const currencyRatesList: CurrencyInfo[] = [
{ code: 'NGN', symbol: '₦', rate: 1, name: 'Nigerian Naira' },
{ code: 'USD', symbol: '$', rate: 0.00063, name: 'US Dollar' },
{ code: 'GBP', symbol: '£', rate: 0.00050, name: 'British Pound' },
{ code: 'EUR', symbol: '€', rate: 0.00058, name: 'Euro' },
{ code: 'GHS', symbol: 'GH₵', rate: 0.0094, name: 'Ghanaian Cedi' },
{ code: 'KES', symbol: 'KSh', rate: 0.093, name: 'Kenyan Shilling' },
{ code: 'ZAR', symbol: 'R', rate: 0.012, name: 'South African Rand' },
{ code: 'UGX', symbol: 'USh', rate: 2.5, name: 'Ugandan Shilling' },
{ code: 'TZS', symbol: 'TSh', rate: 1.6, name: 'Tanzanian Shilling' },
{ code: 'RWF', symbol: 'RF', rate: 0.82, name: 'Rwandan Franc' },
{ code: 'XOF', symbol: 'CFA', rate: 0.38, name: 'West African CFA franc' },
{ code: 'XAF', symbol: 'FCFA', rate: 0.38, name: 'Central African CFA franc' },
{ code: 'CAD', symbol: 'CA$', rate: 0.00086, name: 'Canadian Dollar' },
{ code: 'EGP', symbol: 'E£', rate: 0.030, name: 'Egyptian Pound' },
{ code: 'GNF', symbol: 'FG', rate: 5.4, name: 'Guinean Franc' },
{ code: 'MAD', symbol: 'MAD', rate: 0.0063, name: 'Moroccan Dirham' },
{ code: 'MWK', symbol: 'MK', rate: 1.1, name: 'Malawian Kwacha' },
{ code: 'SLL', symbol: 'Le', rate: 14.0, name: 'Sierra Leonean Leone (New)'},
{ code: 'STD', symbol: 'Db', rate: 14.0, name: 'São Tomé & Príncipe Dobra (New)' },
{ code: 'ZMW', symbol: 'ZK', rate: 0.017, name: 'Zambian Kwacha' },
{ code: 'CLP', symbol: 'CLP$', rate: 0.58, name: 'Chilean Peso' },
{ code: 'COP', symbol: 'COL$', rate: 2.5, name: 'Colombian Peso' },
];
export function CoinPurchaseForm() {
const { toast } = useToast();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [currentUser, setCurrentUser] = useState<LoggedInUser | null>(null);
useEffect(() => {
getLoggedInUser().then(setCurrentUser);
}, []);
const form = useForm<CoinPurchaseInput>({
resolver: zodResolver(CoinPurchaseSchema),
defaultValues: {
package: "medium_150",
currency: "NGN",
paymentGateway: "paystack",
email: currentUser?.email || "",
name: currentUser?.name || "",
},
});
const selectedPackageId = form.watch("package");
const selectedCurrencyCode = form.watch("currency");
const selectedPkg = coinPackages.find(p => p.id === selectedPackageId) || coinPackages[0];
const selectedCurrInfo = currencyRatesList.find(c => c.code === selectedCurrencyCode) || currencyRatesList[0];
const priceInSelectedCurrency = parseFloat((selectedPkg.priceNGN * selectedCurrInfo.rate).toFixed(2));
useEffect(() => {
form.setValue("amountInSelectedCurrency", priceInSelectedCurrency);
form.setValue("amountInNGN", selectedPkg.priceNGN);
form.setValue("coinsToCredit", selectedPkg.coins);
if (currentUser) {
form.setValue("email", currentUser.email);
form.setValue("name", currentUser.name);
}
}, [selectedPkg, priceInSelectedCurrency, form, currentUser]);
const handlePaymentSuccess = async (response: any, gateway: 'paystack' | 'flutterwave') => {
console.log(`${gateway} success response:`, response);
toast({ title: `${gateway} Payment Submitted (Simulation)`, description: `Ref: ${response.reference || response.transaction_id}. Verifying...` });
setIsLoading(true);
// SIMULATE webhook verification for now
const verificationResult = await verifyPaymentAndAwardCoins(gateway, response.reference || response.transaction_id, response);
toast({
title: verificationResult.success ? "Purchase Complete!" : "Verification Issue",
description: verificationResult.message,
variant: verificationResult.success ? "default" : "destructive",
});
if (verificationResult.success) {
router.refresh();
}
setIsLoading(false);
};
const handlePaymentClose = (gateway: 'paystack' | 'flutterwave') => {
console.log(`${gateway} payment modal closed.`);
toast({ title: "Payment Cancelled", description: "The payment process was cancelled.", variant: "default" });
setIsLoading(false);
};
// Paystack Config
const paystackConfig = {
reference: new Date().getTime().toString(),
email: form.getValues("email"),
amount: priceInSelectedCurrency * 100, // Amount in kobo
currency: selectedCurrencyCode,
publicKey: paystackPublicKey,
metadata: {
userId: currentUser?._id,
packageName: selectedPkg.name,
coins: selectedPkg.coins,
custom_fields: [
{
display_name: "Package",
variable_name: "package",
value: selectedPkg.name
},
{
display_name: "Coins",
variable_name: "coins",
value: selectedPkg.coins
}
]
}
};
// Flutterwave Config
const flutterwaveConfig = {
public_key: flutterwavePublicKey,
tx_ref: new Date().getTime().toString(),
amount: priceInSelectedCurrency,
currency: selectedCurrencyCode,
payment_options: "card,mobilemoney,ussd",
customer: {
email: form.getValues("email"),
name: form.getValues("name") || "Anita Deploy User",
},
customizations: {
title: "Anita Deploy - Coin Purchase",
description: `Payment for ${selectedPkg.coins} coins`,
logo: "https://placehold.co/100x100.png?text=AD",
},
};
const initializePaystackPayment = usePaystackPayment(paystackConfig);
const handleFlutterwavePayment = useFlutterwave(flutterwaveConfig);
async function onSubmit(values: CoinPurchaseInput) {
setIsLoading(true);
if (!paystackPublicKey || !flutterwavePublicKey) {
toast({title: "Configuration Error", description: "Payment gateway keys are not set. Please contact support.", variant: "destructive"});
setIsLoading(false);
return;
}
if (!currentUser) {
toast({title: "Error", description: "User not loaded. Please refresh.", variant: "destructive"});
setIsLoading(false);
return;
}
const currentPaystackConfig = {
...paystackConfig,
reference: `anitad_${currentUser._id}_${new Date().getTime()}`,
email: values.email,
amount: values.amountInSelectedCurrency * 100,
currency: values.currency,
metadata: {
userId: currentUser._id,
packageName: selectedPkg.name,
coins: selectedPkg.coins,
transactionType: "coin_purchase",
custom_fields: [
{ display_name: "Package", variable_name: "package", value: selectedPkg.name },
{ display_name: "Coins", variable_name: "coins", value: selectedPkg.coins }
]
}
};
const currentFlutterwaveConfig = {
...flutterwaveConfig,
tx_ref: `anitad_${currentUser._id}_${new Date().getTime()}`,
amount: values.amountInSelectedCurrency,
currency: values.currency,
customer: {
email: values.email,
name: values.name || "Anita Deploy User",
},
meta: {
userId: currentUser._id,
packageName: selectedPkg.name,
coins: selectedPkg.coins,
transactionType: "coin_purchase"
}
};
const initResult = await initiateCoinPurchase({
...values,
});
if (!initResult.success || !initResult.transactionReference) {
toast({ title: "Initiation Failed", description: initResult.message, variant: "destructive" });
setIsLoading(false);
return;
}
currentPaystackConfig.reference = initResult.transactionReference;
currentFlutterwaveConfig.tx_ref = initResult.transactionReference;
if (values.paymentGateway === "paystack") {
initializePaystackPayment({
onSuccess: (response) => handlePaymentSuccess(response, 'paystack'),
onClose: () => handlePaymentClose('paystack'),
config: currentPaystackConfig,
});
} else if (values.paymentGateway === "flutterwave") {
handleFlutterwavePayment({
callback: (response) => {
handlePaymentSuccess(response, 'flutterwave');
closePaymentModal();
},
onClose: () => handlePaymentClose('flutterwave'),
});
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="package"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel className="text-lg font-semibold">1. Select Coin Package</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
{coinPackages.map((pkg) => (
<FormItem key={pkg.id} className="flex-1">
<FormControl>
<RadioGroupItem value={pkg.id} id={pkg.id} className="sr-only" />
</FormControl>
<Label
htmlFor={pkg.id}
className={`flex flex-col items-center justify-between rounded-lg border-2 bg-card p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer transition-all
${field.value === pkg.id ? "border-primary ring-2 ring-primary shadow-lg" : "border-muted"}`}
>
<div className="flex items-center text-xl font-semibold mb-2">
<Coins className="mr-2 h-6 w-6 text-yellow-500" /> {pkg.coins} Coins
</div>
<p className="text-sm font-bold text-primary">{pkg.name}</p>
<p className="text-xs text-muted-foreground mt-1">{pkg.description}</p>
<p className="text-lg font-semibold mt-3 text-foreground">
{currencyRatesList.find(c => c.code === 'NGN')?.symbol}
{pkg.priceNGN.toLocaleString()}
</p>
</Label>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="currency"
render={({ field }) => (
<FormItem>
<FormLabel className="text-lg font-semibold">2. Select Currency</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full md:w-[280px]">
<SelectValue placeholder="Select currency" />
</SelectTrigger>
</FormControl>
<SelectContent>
{currencyRatesList.map((currency) => (
<SelectItem key={currency.code} value={currency.code}>
{currency.name} ({currency.symbol})
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
The price will be converted to your selected currency.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Card className="bg-secondary/50 shadow-md">
<CardHeader>
<CardTitle className="text-xl">Order Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-lg">
<span className="font-medium text-muted-foreground">Package:</span> {selectedPkg.name} ({selectedPkg.coins} Coins)
</p>
<p className="text-2xl font-bold text-primary">
<span className="font-medium text-muted-foreground">Total:</span> {selectedCurrInfo.symbol}
{priceInSelectedCurrency.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
<span className="text-sm font-normal text-muted-foreground ml-1"> ({selectedCurrencyCode})</span>
</p>
{selectedCurrencyCode !== 'NGN' && (
<p className="text-sm text-muted-foreground">
(Approx. {currencyRatesList.find(c => c.code === 'NGN')?.symbol}
{selectedPkg.priceNGN.toLocaleString()} NGN)
</p>
)}
</CardContent>
</Card>
<FormField
control={form.control}
name="paymentGateway"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel className="text-lg font-semibold">3. Select Payment Gateway</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col sm:flex-row gap-4"
>
<FormItem className="flex-1">
<FormControl>
<RadioGroupItem value="paystack" id="paystack" className="sr-only" />
</FormControl>
<Label htmlFor="paystack" className={`flex items-center justify-center rounded-lg border-2 p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${field.value === "paystack" ? "border-primary ring-2 ring-primary" : "border-muted"}`}>
<img src="https://assets.paystack.com/assets/img/logos/paystack-logo-vector-deep-blue.svg" alt="Paystack" className="h-7" data-ai-hint="paystack logo"/>
</Label>
</FormItem>
<FormItem className="flex-1">
<FormControl>
<RadioGroupItem value="flutterwave" id="flutterwave" className="sr-only" />
</FormControl>
<Label htmlFor="flutterwave" className={`flex items-center justify-center rounded-lg border-2 p-4 hover:bg-accent hover:text-accent-foreground cursor-pointer ${field.value === "flutterwave" ? "border-primary ring-2 ring-primary" : "border-muted"}`}>
<img src="https://flutterwave.com/images/logo-colored.svg" alt="Flutterwave" className="h-7" data-ai-hint="flutterwave logo"/>
</Label>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" size="lg" className="w-full text-base py-6" disabled={isLoading}>
{isLoading ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<ShoppingCart className="mr-2 h-5 w-5" />
)}
Proceed to Payment
</Button>
<p className="text-xs text-muted-foreground text-center flex items-center justify-center">
<Lock className="h-3 w-3 mr-1.5"/> Secure payment processing by Paystack & Flutterwave.
</p>
</form>
</Form>
);
}