| |
|
| | "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; |
| |
|
| | 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); |
| | |
| | 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); |
| | }; |
| | |
| | |
| | const paystackConfig = { |
| | reference: new Date().getTime().toString(), |
| | email: form.getValues("email"), |
| | amount: priceInSelectedCurrency * 100, |
| | 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 |
| | } |
| | ] |
| | } |
| | }; |
| |
|
| | |
| | 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> |
| | ); |
| | } |
| |
|
| |
|