/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type ChangeEvent, useEffect, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { Plus, Pencil, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsPageActionsPortal } from '../components/settings-page-context' import { useUpdateOption } from '../hooks/use-update-option' export interface WaffoSettingsValues { WaffoEnabled: boolean WaffoApiKey: string WaffoPrivateKey: string WaffoPublicCert: string WaffoSandboxPublicCert: string WaffoSandboxApiKey: string WaffoSandboxPrivateKey: string WaffoSandbox: boolean WaffoMerchantId: string WaffoCurrency: string WaffoUnitPrice: number WaffoMinTopUp: number WaffoNotifyUrl: string WaffoReturnUrl: string WaffoPayMethods: string } interface PayMethod { name: string icon: string payMethodType: string payMethodName: string } interface Props { defaultValues: WaffoSettingsValues } export function WaffoSettingsSection(props: Props) { const { t } = useTranslation() const updateOption = useUpdateOption() const [loading, setLoading] = useState(false) const iconFileInputRef = useRef(null) const form = useForm>({ defaultValues: props.defaultValues, }) const [payMethods, setPayMethods] = useState(() => { try { return JSON.parse(props.defaultValues.WaffoPayMethods || '[]') } catch { return [] } }) const [methodDialogOpen, setMethodDialogOpen] = useState(false) const [editingIdx, setEditingIdx] = useState(-1) const [methodForm, setMethodForm] = useState({ name: '', icon: '', payMethodType: '', payMethodName: '', }) useEffect(() => { form.reset(props.defaultValues) try { setPayMethods(JSON.parse(props.defaultValues.WaffoPayMethods || '[]')) } catch { setPayMethods([]) } }, [props.defaultValues, form]) const handleSave = async () => { setLoading(true) try { const values = form.getValues() const options: { key: string; value: string }[] = [ { key: 'WaffoEnabled', value: String(values.WaffoEnabled) }, { key: 'WaffoSandbox', value: String(values.WaffoSandbox) }, { key: 'WaffoMerchantId', value: values.WaffoMerchantId || '' }, { key: 'WaffoCurrency', value: values.WaffoCurrency || 'USD' }, { key: 'WaffoUnitPrice', value: String(values.WaffoUnitPrice || 1) }, { key: 'WaffoMinTopUp', value: String(values.WaffoMinTopUp || 1) }, { key: 'WaffoNotifyUrl', value: values.WaffoNotifyUrl || '' }, { key: 'WaffoReturnUrl', value: values.WaffoReturnUrl || '' }, { key: 'WaffoPublicCert', value: values.WaffoPublicCert || '' }, { key: 'WaffoSandboxPublicCert', value: values.WaffoSandboxPublicCert || '', }, { key: 'WaffoPayMethods', value: JSON.stringify(payMethods) }, ] if (values.WaffoApiKey) options.push({ key: 'WaffoApiKey', value: values.WaffoApiKey }) if (values.WaffoPrivateKey) options.push({ key: 'WaffoPrivateKey', value: values.WaffoPrivateKey }) if (values.WaffoSandboxApiKey) options.push({ key: 'WaffoSandboxApiKey', value: values.WaffoSandboxApiKey, }) if (values.WaffoSandboxPrivateKey) options.push({ key: 'WaffoSandboxPrivateKey', value: values.WaffoSandboxPrivateKey, }) for (const opt of options) { await updateOption.mutateAsync(opt) } toast.success(t('Updated successfully')) } catch { toast.error(t('Update failed')) } finally { setLoading(false) } } const openAdd = () => { setEditingIdx(-1) setMethodForm({ name: '', icon: '', payMethodType: '', payMethodName: '' }) setMethodDialogOpen(true) } const openEdit = (idx: number) => { setEditingIdx(idx) setMethodForm({ ...payMethods[idx] }) setMethodDialogOpen(true) } const saveMethod = () => { if (!methodForm.name.trim()) return toast.error(t('Payment method name is required')) if (editingIdx === -1) { setPayMethods((prev) => [...prev, methodForm]) } else { setPayMethods((prev) => prev.map((m, i) => (i === editingIdx ? methodForm : m)) ) } setMethodDialogOpen(false) } const handleIconFileChange = (event: ChangeEvent) => { const file = event.target.files?.[0] if (!file) { return } const maxIconSize = 100 * 1024 if (file.size > maxIconSize) { toast.error(t('Icon file must be 100 KB or smaller')) event.target.value = '' return } const reader = new FileReader() reader.onload = (loadEvent) => { setMethodForm((previous) => ({ ...previous, icon: typeof loadEvent.target?.result === 'string' ? loadEvent.target.result : '', })) } reader.readAsDataURL(file) event.target.value = '' } return ( <> {loading ? t('Saving...') : t('Save Waffo settings')} {t('Waffo Aggregator Gateway')} {t( 'Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.' )} {t( 'Obtain the API key, merchant ID, and RSA key pair from the Waffo dashboard, and configure the callback URL.' )} form.setValue('WaffoEnabled', v)} label={t('Enable Waffo')} className='border-b-0 py-0' /> form.setValue('WaffoSandbox', v)} label={t('Sandbox mode')} className='border-b-0 py-0' /> {t('API Key (Production)')} {t('API Key (Sandbox)')} {t('Merchant ID')} {t('RSA Private Key (Production)')} {t('RSA Private Key (Sandbox)')} {t('Waffo Public Key (Production)')} {t('Waffo Public Key (Sandbox)')} {t('Currency')} {t('Unit price (USD)')} {t('Minimum top-up quantity')} {t('Callback notification URL')} {t('Payment return URL')} {t('Payment Methods')} {t('Add payment method')} {t('Display name')} {t('Icon')} {t('Payment method type')} {t('Payment method name')} {t('Actions')} {payMethods.length === 0 ? ( {t('No payment methods configured')} ) : ( payMethods.map((m, idx) => ( {m.name} {m.icon ? ( ) : ( - )} {m.payMethodType || '-'} {m.payMethodName || '-'} openEdit(idx)} > setPayMethods((prev) => prev.filter((_, i) => i !== idx) ) } > )) )} {editingIdx === -1 ? t('Add payment method') : t('Edit payment method')} {t('Display name')} * setMethodForm((p) => ({ ...p, name: e.target.value })) } /> {t('Icon')} {methodForm.icon ? ( ) : ( {t('Icon')} )} iconFileInputRef.current?.click()} > {t('Upload')} {methodForm.icon ? ( setMethodForm((previous) => ({ ...previous, icon: '', })) } > {t('Clear')} ) : null} {t( 'Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.' )} {t('Payment method type')} setMethodForm((p) => ({ ...p, payMethodType: e.target.value, })) } placeholder='CREDITCARD,DEBITCARD' /> {t('Payment method name')} setMethodForm((p) => ({ ...p, payMethodName: e.target.value, })) } /> setMethodDialogOpen(false)} > {t('Cancel')} {t('Confirm')} > ) }
{t( 'Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.' )}
{t( 'Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.' )}