diff --git a/web/default/src/features/system-settings/integrations/payment-settings-section.tsx b/web/default/src/features/system-settings/integrations/payment-settings-section.tsx index 0a6fe31d..0ec1467f 100644 --- a/web/default/src/features/system-settings/integrations/payment-settings-section.tsx +++ b/web/default/src/features/system-settings/integrations/payment-settings-section.tsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' import * as z from 'zod' -import { useForm } from 'react-hook-form' +import { useForm, type Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' import { Code2, Eye, ShieldAlert } from 'lucide-react' @@ -65,11 +65,14 @@ import { normalizeJsonForComparison, removeTrailingSlash, } from './utils' +import { saveWaffoPancakeConfig } from './waffo-pancake-api' import { WaffoPancakeSettingsSection, + type WaffoPancakeBinding, type WaffoPancakeSettingsValues, } from './waffo-pancake-settings-section' import { + type PayMethod, WaffoSettingsSection, type WaffoSettingsValues, } from './waffo-settings-section' @@ -138,9 +141,31 @@ const paymentSchema = z.object({ }) } }), + WaffoEnabled: z.boolean(), + WaffoApiKey: z.string(), + WaffoPrivateKey: z.string(), + WaffoPublicCert: z.string(), + WaffoSandboxPublicCert: z.string(), + WaffoSandboxApiKey: z.string(), + WaffoSandboxPrivateKey: z.string(), + WaffoSandbox: z.boolean(), + WaffoMerchantId: z.string(), + WaffoCurrency: z.string(), + WaffoUnitPrice: z.coerce.number().min(0), + WaffoMinTopUp: z.coerce.number().min(1), + WaffoNotifyUrl: z.string(), + WaffoReturnUrl: z.string(), + WaffoPancakeMerchantID: z.string(), + WaffoPancakePrivateKey: z.string(), + WaffoPancakeReturnURL: z.string(), }) type PaymentFormValues = z.infer +type WaffoFormFieldValues = Omit +type PaymentBaseFormValues = Omit< + PaymentFormValues, + keyof WaffoFormFieldValues | keyof WaffoPancakeSettingsValues +> const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1' @@ -152,7 +177,7 @@ type PaymentComplianceDefaults = { } type PaymentSettingsSectionProps = { - defaultValues: PaymentFormValues + defaultValues: PaymentBaseFormValues waffoDefaultValues: WaffoSettingsValues waffoPancakeDefaultValues: WaffoPancakeSettingsValues waffoPancakeProvisionedStoreID?: string @@ -160,6 +185,15 @@ type PaymentSettingsSectionProps = { complianceDefaults: PaymentComplianceDefaults } +function parseWaffoPayMethods(value: string): PayMethod[] { + try { + const parsed = JSON.parse(value || '[]') + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + export function PaymentSettingsSection({ defaultValues, waffoDefaultValues, @@ -171,10 +205,18 @@ export function PaymentSettingsSection({ const { t } = useTranslation() const queryClient = useQueryClient() const updateOption = useUpdateOption() - const initialRef = React.useRef(defaultValues) + const initialFormValues = React.useMemo( + () => ({ + ...defaultValues, + ...waffoDefaultValues, + ...waffoPancakeDefaultValues, + }), + [defaultValues, waffoDefaultValues, waffoPancakeDefaultValues] + ) + const initialRef = React.useRef(initialFormValues) const defaultsSignature = React.useMemo( - () => JSON.stringify(defaultValues), - [defaultValues] + () => JSON.stringify(initialFormValues), + [initialFormValues] ) const [payMethodsVisualMode, setPayMethodsVisualMode] = React.useState(true) @@ -185,6 +227,32 @@ export function PaymentSettingsSection({ const [creemProductsVisualMode, setCreemProductsVisualMode] = React.useState(true) const [showComplianceDialog, setShowComplianceDialog] = React.useState(false) + const [waffoPayMethods, setWaffoPayMethods] = React.useState( + () => parseWaffoPayMethods(waffoDefaultValues.WaffoPayMethods) + ) + const [waffoPancakeSelection, setWaffoPancakeSelection] = + React.useState({ + storeID: waffoPancakeProvisionedStoreID ?? '', + productID: waffoPancakeProvisionedProductID ?? '', + }) + const [waffoPancakeSavedBinding, setWaffoPancakeSavedBinding] = + React.useState({ + storeID: waffoPancakeProvisionedStoreID ?? '', + productID: waffoPancakeProvisionedProductID ?? '', + }) + + React.useEffect(() => { + setWaffoPayMethods(parseWaffoPayMethods(waffoDefaultValues.WaffoPayMethods)) + }, [waffoDefaultValues.WaffoPayMethods]) + + React.useEffect(() => { + const nextBinding = { + storeID: waffoPancakeProvisionedStoreID ?? '', + productID: waffoPancakeProvisionedProductID ?? '', + } + setWaffoPancakeSelection(nextBinding) + setWaffoPancakeSavedBinding(nextBinding) + }, [waffoPancakeProvisionedProductID, waffoPancakeProvisionedStoreID]) const complianceStatements = React.useMemo( () => [ @@ -260,18 +328,63 @@ export function PaymentSettingsSection({ }, }) - const form = useForm({ - resolver: zodResolver(paymentSchema), + const form = useForm({ + resolver: zodResolver(paymentSchema) as Resolver, mode: 'onChange', // Enable real-time validation defaultValues: { - ...defaultValues, - PayMethods: formatJsonForEditor(defaultValues.PayMethods), - AmountOptions: formatJsonForEditor(defaultValues.AmountOptions), - AmountDiscount: formatJsonForEditor(defaultValues.AmountDiscount), - CreemProducts: formatJsonForEditor(defaultValues.CreemProducts), + ...initialFormValues, + PayMethods: formatJsonForEditor(initialFormValues.PayMethods), + AmountOptions: formatJsonForEditor(initialFormValues.AmountOptions), + AmountDiscount: formatJsonForEditor(initialFormValues.AmountDiscount), + CreemProducts: formatJsonForEditor(initialFormValues.CreemProducts), }, }) + const { isSubmitting } = form.formState + + const setPaymentValue = React.useCallback( + ( + key: keyof PaymentFormValues, + value: PaymentFormValues[keyof PaymentFormValues] + ) => { + form.setValue( + key as Parameters[0], + value as Parameters[1], + { + shouldDirty: true, + shouldValidate: true, + } + ) + }, + [form] + ) + + const setWaffoValue = React.useCallback( + ( + key: K, + value: WaffoFormFieldValues[K] + ) => { + setPaymentValue( + key as keyof PaymentFormValues, + value as PaymentFormValues[keyof PaymentFormValues] + ) + }, + [setPaymentValue] + ) + + const setWaffoPancakeValue = React.useCallback( + ( + key: K, + value: WaffoPancakeSettingsValues[K] + ) => { + setPaymentValue( + key as keyof PaymentFormValues, + value as PaymentFormValues[keyof PaymentFormValues] + ) + }, + [setPaymentValue] + ) + React.useEffect(() => { const parsedDefaults = JSON.parse(defaultsSignature) as PaymentFormValues initialRef.current = parsedDefaults @@ -305,6 +418,26 @@ export function PaymentSettingsSection({ CreemWebhookSecret: values.CreemWebhookSecret.trim(), CreemTestMode: values.CreemTestMode, CreemProducts: values.CreemProducts.trim(), + WaffoEnabled: values.WaffoEnabled, + WaffoSandbox: values.WaffoSandbox, + WaffoMerchantId: values.WaffoMerchantId.trim(), + WaffoCurrency: values.WaffoCurrency.trim() || 'USD', + WaffoUnitPrice: values.WaffoUnitPrice, + WaffoMinTopUp: values.WaffoMinTopUp, + WaffoNotifyUrl: values.WaffoNotifyUrl.trim(), + WaffoReturnUrl: values.WaffoReturnUrl.trim(), + WaffoPublicCert: values.WaffoPublicCert.trim(), + WaffoSandboxPublicCert: values.WaffoSandboxPublicCert.trim(), + WaffoApiKey: values.WaffoApiKey.trim(), + WaffoPrivateKey: values.WaffoPrivateKey.trim(), + WaffoSandboxApiKey: values.WaffoSandboxApiKey.trim(), + WaffoSandboxPrivateKey: values.WaffoSandboxPrivateKey.trim(), + WaffoPayMethods: JSON.stringify(waffoPayMethods), + WaffoPancakeMerchantID: values.WaffoPancakeMerchantID.trim(), + WaffoPancakePrivateKey: values.WaffoPancakePrivateKey.trim(), + WaffoPancakeReturnURL: removeTrailingSlash( + values.WaffoPancakeReturnURL.trim() + ), } const initial = { @@ -330,6 +463,28 @@ export function PaymentSettingsSection({ CreemWebhookSecret: initialRef.current.CreemWebhookSecret.trim(), CreemTestMode: initialRef.current.CreemTestMode, CreemProducts: initialRef.current.CreemProducts.trim(), + WaffoEnabled: initialRef.current.WaffoEnabled, + WaffoSandbox: initialRef.current.WaffoSandbox, + WaffoMerchantId: initialRef.current.WaffoMerchantId.trim(), + WaffoCurrency: initialRef.current.WaffoCurrency.trim() || 'USD', + WaffoUnitPrice: initialRef.current.WaffoUnitPrice, + WaffoMinTopUp: initialRef.current.WaffoMinTopUp, + WaffoNotifyUrl: initialRef.current.WaffoNotifyUrl.trim(), + WaffoReturnUrl: initialRef.current.WaffoReturnUrl.trim(), + WaffoPublicCert: initialRef.current.WaffoPublicCert.trim(), + WaffoSandboxPublicCert: initialRef.current.WaffoSandboxPublicCert.trim(), + WaffoApiKey: initialRef.current.WaffoApiKey.trim(), + WaffoPrivateKey: initialRef.current.WaffoPrivateKey.trim(), + WaffoSandboxApiKey: initialRef.current.WaffoSandboxApiKey.trim(), + WaffoSandboxPrivateKey: initialRef.current.WaffoSandboxPrivateKey.trim(), + WaffoPayMethods: JSON.stringify( + parseWaffoPayMethods(waffoDefaultValues.WaffoPayMethods) + ), + WaffoPancakeMerchantID: initialRef.current.WaffoPancakeMerchantID.trim(), + WaffoPancakePrivateKey: initialRef.current.WaffoPancakePrivateKey.trim(), + WaffoPancakeReturnURL: removeTrailingSlash( + initialRef.current.WaffoPancakeReturnURL.trim() + ), } const updates: Array<{ key: string; value: string | number | boolean }> = [] @@ -455,9 +610,171 @@ export function PaymentSettingsSection({ updates.push({ key: 'CreemProducts', value: sanitized.CreemProducts }) } + if (sanitized.WaffoEnabled !== initial.WaffoEnabled) { + updates.push({ key: 'WaffoEnabled', value: sanitized.WaffoEnabled }) + } + + if (sanitized.WaffoSandbox !== initial.WaffoSandbox) { + updates.push({ key: 'WaffoSandbox', value: sanitized.WaffoSandbox }) + } + + if (sanitized.WaffoMerchantId !== initial.WaffoMerchantId) { + updates.push({ key: 'WaffoMerchantId', value: sanitized.WaffoMerchantId }) + } + + if (sanitized.WaffoCurrency !== initial.WaffoCurrency) { + updates.push({ key: 'WaffoCurrency', value: sanitized.WaffoCurrency }) + } + + if (sanitized.WaffoUnitPrice !== initial.WaffoUnitPrice) { + updates.push({ key: 'WaffoUnitPrice', value: sanitized.WaffoUnitPrice }) + } + + if (sanitized.WaffoMinTopUp !== initial.WaffoMinTopUp) { + updates.push({ key: 'WaffoMinTopUp', value: sanitized.WaffoMinTopUp }) + } + + if (sanitized.WaffoNotifyUrl !== initial.WaffoNotifyUrl) { + updates.push({ key: 'WaffoNotifyUrl', value: sanitized.WaffoNotifyUrl }) + } + + if (sanitized.WaffoReturnUrl !== initial.WaffoReturnUrl) { + updates.push({ key: 'WaffoReturnUrl', value: sanitized.WaffoReturnUrl }) + } + + if (sanitized.WaffoPublicCert !== initial.WaffoPublicCert) { + updates.push({ key: 'WaffoPublicCert', value: sanitized.WaffoPublicCert }) + } + + if (sanitized.WaffoSandboxPublicCert !== initial.WaffoSandboxPublicCert) { + updates.push({ + key: 'WaffoSandboxPublicCert', + value: sanitized.WaffoSandboxPublicCert, + }) + } + + if (sanitized.WaffoApiKey) { + updates.push({ key: 'WaffoApiKey', value: sanitized.WaffoApiKey }) + } + + if (sanitized.WaffoPrivateKey) { + updates.push({ key: 'WaffoPrivateKey', value: sanitized.WaffoPrivateKey }) + } + + if (sanitized.WaffoSandboxApiKey) { + updates.push({ + key: 'WaffoSandboxApiKey', + value: sanitized.WaffoSandboxApiKey, + }) + } + + if (sanitized.WaffoSandboxPrivateKey) { + updates.push({ + key: 'WaffoSandboxPrivateKey', + value: sanitized.WaffoSandboxPrivateKey, + }) + } + + if ( + normalizeJsonForComparison(sanitized.WaffoPayMethods) !== + normalizeJsonForComparison(initial.WaffoPayMethods) + ) { + updates.push({ key: 'WaffoPayMethods', value: sanitized.WaffoPayMethods }) + } + + const hasWaffoPancakeChanges = + sanitized.WaffoPancakeMerchantID !== initial.WaffoPancakeMerchantID || + sanitized.WaffoPancakePrivateKey.length > 0 || + sanitized.WaffoPancakeReturnURL !== initial.WaffoPancakeReturnURL || + waffoPancakeSelection.storeID !== waffoPancakeSavedBinding.storeID || + waffoPancakeSelection.productID !== waffoPancakeSavedBinding.productID + + if (updates.length === 0 && !hasWaffoPancakeChanges) { + toast.info(t('No changes to save')) + return + } + for (const update of updates) { await updateOption.mutateAsync(update) } + + if (!hasWaffoPancakeChanges) { + return + } + + if (!sanitized.WaffoPancakeMerchantID) { + toast.error(t('Merchant ID is required')) + return + } + + if (!waffoPancakeSelection.storeID || !waffoPancakeSelection.productID) { + toast.error(t('Pick or create both a store and a product before saving.')) + return + } + + try { + const body = await saveWaffoPancakeConfig({ + merchantID: sanitized.WaffoPancakeMerchantID, + privateKey: sanitized.WaffoPancakePrivateKey, + returnURL: sanitized.WaffoPancakeReturnURL, + storeID: waffoPancakeSelection.storeID, + productID: waffoPancakeSelection.productID, + }) + + if ( + body?.message === 'success' && + typeof body.data === 'object' && + body.data + ) { + const saved = body.data as { product_id: string; store_id: string } + const savedBinding = { + storeID: saved.store_id, + productID: saved.product_id, + } + setWaffoPancakeSavedBinding(savedBinding) + setWaffoPancakeSelection(savedBinding) + queryClient.invalidateQueries({ queryKey: ['system-options'] }) + toast.success(t('Waffo Pancake settings saved')) + return + } + + const reason = typeof body?.data === 'string' ? body.data : undefined + toast.error( + reason + ? `${t('Waffo Pancake save failed')}: ${reason}` + : t('Waffo Pancake save failed') + ) + } catch (error) { + toast.error( + `${t('Waffo Pancake save failed')}: ${ + error instanceof Error ? error.message : String(error) + }` + ) + } + } + + const currentFormValues = form.watch() + const waffoValues: WaffoSettingsValues = { + WaffoEnabled: currentFormValues.WaffoEnabled, + WaffoApiKey: currentFormValues.WaffoApiKey, + WaffoPrivateKey: currentFormValues.WaffoPrivateKey, + WaffoPublicCert: currentFormValues.WaffoPublicCert, + WaffoSandboxPublicCert: currentFormValues.WaffoSandboxPublicCert, + WaffoSandboxApiKey: currentFormValues.WaffoSandboxApiKey, + WaffoSandboxPrivateKey: currentFormValues.WaffoSandboxPrivateKey, + WaffoSandbox: currentFormValues.WaffoSandbox, + WaffoMerchantId: currentFormValues.WaffoMerchantId, + WaffoCurrency: currentFormValues.WaffoCurrency, + WaffoUnitPrice: currentFormValues.WaffoUnitPrice, + WaffoMinTopUp: currentFormValues.WaffoMinTopUp, + WaffoNotifyUrl: currentFormValues.WaffoNotifyUrl, + WaffoReturnUrl: currentFormValues.WaffoReturnUrl, + WaffoPayMethods: JSON.stringify(waffoPayMethods), + } + const waffoPancakeValues: WaffoPancakeSettingsValues = { + WaffoPancakeMerchantID: currentFormValues.WaffoPancakeMerchantID, + WaffoPancakePrivateKey: currentFormValues.WaffoPancakePrivateKey, + WaffoPancakeReturnURL: currentFormValues.WaffoPancakeReturnURL, } return ( @@ -525,7 +842,6 @@ export function PaymentSettingsSection({ onConfirm={() => confirmComplianceMutation.mutate()} /> - {/* eslint-disable react-hooks/refs */}
@@ -1206,21 +1522,28 @@ export function PaymentSettingsSection({ )} />
+ + + + + + + +
- - - - - - - - - {/* eslint-enable react-hooks/refs */} ) } diff --git a/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx b/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx index e42a6bc2..82ad963a 100644 --- a/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx +++ b/web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx @@ -17,20 +17,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import * as z from 'zod' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' +import type { SetStateAction } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -41,8 +31,6 @@ import { SelectValue, } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' -import { SettingsForm } from '../components/settings-form-layout' -import { SettingsPageActionsPortal } from '../components/settings-page-context' import { removeTrailingSlash } from './utils' import { type CatalogStore, @@ -50,23 +38,29 @@ import { type PairResult, createWaffoPancakePair, listWaffoPancakeCatalog, - saveWaffoPancakeConfig, } from './waffo-pancake-api' -// Only operator-typed fields. Nothing else lands in OptionMap until Save. -const waffoPancakeSchema = z.object({ - WaffoPancakeMerchantID: z.string(), - WaffoPancakePrivateKey: z.string(), -}) - -export type WaffoPancakeSettingsValues = z.infer & { +export type WaffoPancakeSettingsValues = { + WaffoPancakeMerchantID: string + WaffoPancakePrivateKey: string WaffoPancakeReturnURL: string } +export interface WaffoPancakeBinding { + storeID: string + productID: string +} + interface Props { defaultValues: WaffoPancakeSettingsValues - provisionedStoreID?: string - provisionedProductID?: string + values: WaffoPancakeSettingsValues + onValueChange: ( + key: K, + value: WaffoPancakeSettingsValues[K] + ) => void + selectedBinding: WaffoPancakeBinding + savedBinding: WaffoPancakeBinding + onSelectedBindingChange: (value: SetStateAction) => void } const PANCAKE_DASHBOARD_URL = 'https://pancake.waffo.ai/merchant/dashboard' @@ -74,35 +68,29 @@ const DEFAULT_NEW_STORE_NAME = 'new-api-store' const DEFAULT_NEW_PRODUCT_NAME = 'new-api-charge-product' const DEFAULT_NEW_PAIR_NAME = `${DEFAULT_NEW_STORE_NAME} + ${DEFAULT_NEW_PRODUCT_NAME}` -export function WaffoPancakeSettingsSection(props: Props) { +export function WaffoPancakeSettingsSection({ + defaultValues, + values, + onValueChange, + selectedBinding, + savedBinding, + onSelectedBindingChange, +}: Props) { const { t } = useTranslation() - const [storeID, setStoreID] = React.useState(props.provisionedStoreID ?? '') - const [productID, setProductID] = React.useState( - props.provisionedProductID ?? '' - ) - - const [phase, setPhase] = React.useState<'idle' | 'verifying' | 'saving'>( - 'idle' - ) + const [phase, setPhase] = React.useState<'idle' | 'verifying'>('idle') const [catalog, setCatalog] = React.useState([]) - // Seed dropdowns from saved bindings so they render on first paint instead - // of waiting for the async catalog fetch to confirm them. - const [chosenStoreID, setChosenStoreID] = React.useState( - props.provisionedStoreID ?? '' - ) - const [chosenProductID, setChosenProductID] = React.useState( - props.provisionedProductID ?? '' - ) - const [returnURL, setReturnURL] = React.useState( - props.defaultValues.WaffoPancakeReturnURL ?? '' - ) const [creatingPair, setCreatingPair] = React.useState(false) + const chosenStoreID = selectedBinding.storeID + const chosenProductID = selectedBinding.productID + const storeID = savedBinding.storeID + const productID = savedBinding.productID + const returnURL = values.WaffoPancakeReturnURL - const initialRef = React.useRef(props.defaultValues) + const initialRef = React.useRef(defaultValues) const defaultsSignature = React.useMemo( - () => JSON.stringify(props.defaultValues), - [props.defaultValues] + () => JSON.stringify(defaultValues), + [defaultValues] ) // "merchantID|privateKey" of the last verified pair; debounced verify @@ -110,15 +98,6 @@ export function WaffoPancakeSettingsSection(props: Props) { const lastVerifiedSignature = React.useRef('') const fetchSerialRef = React.useRef(0) - const form = useForm({ - resolver: zodResolver(waffoPancakeSchema), - mode: 'onChange', - defaultValues: { - WaffoPancakeMerchantID: props.defaultValues.WaffoPancakeMerchantID, - WaffoPancakePrivateKey: props.defaultValues.WaffoPancakePrivateKey, - }, - }) - // Mount-only — never re-sync from props after the first render. The // backend strips PrivateKey from GET /api/option/, so a re-sync would // wipe whatever the operator just typed. @@ -128,21 +107,8 @@ export function WaffoPancakeSettingsSection(props: Props) { initialRef.current = parsed if (didMountRef.current) return didMountRef.current = true - form.reset({ - WaffoPancakeMerchantID: parsed.WaffoPancakeMerchantID, - WaffoPancakePrivateKey: parsed.WaffoPancakePrivateKey, - }) - setReturnURL(parsed.WaffoPancakeReturnURL ?? '') lastVerifiedSignature.current = `${parsed.WaffoPancakeMerchantID.trim()}|${parsed.WaffoPancakePrivateKey.trim()}` - }, [defaultsSignature, form]) - - React.useEffect(() => { - setStoreID(props.provisionedStoreID ?? '') - }, [props.provisionedStoreID]) - - React.useEffect(() => { - setProductID(props.provisionedProductID ?? '') - }, [props.provisionedProductID]) + }, [defaultsSignature]) const productsForChosenStore = React.useMemo(() => { if (!chosenStoreID) return [] @@ -185,7 +151,7 @@ export function WaffoPancakeSettingsSection(props: Props) { preselect?: { storeID?: string; productID?: string } ) => { const serial = ++fetchSerialRef.current - let stores: CatalogStore[] = [] + let stores: CatalogStore[] try { const body = await listWaffoPancakeCatalog(merchantID, privateKey) if (serial !== fetchSerialRef.current) return @@ -221,8 +187,10 @@ export function WaffoPancakeSettingsSection(props: Props) { setCatalog(stores) if (preselect) { - setChosenStoreID(preselect.storeID ?? '') - setChosenProductID(preselect.productID ?? '') + onSelectedBindingChange({ + storeID: preselect.storeID ?? '', + productID: preselect.productID ?? '', + }) } else { // Default anchor: bound product if found, else first product of // the first store with any — saves a click for new operators. @@ -230,28 +198,31 @@ export function WaffoPancakeSettingsSection(props: Props) { s.onetimeProducts.some((p) => p.id === productID) ) if (boundStore && productID) { - setChosenStoreID(boundStore.id) - setChosenProductID(productID) + onSelectedBindingChange({ + storeID: boundStore.id, + productID, + }) } else { const storeWithProducts = stores.find( (s) => s.onetimeProducts.length > 0 ) if (storeWithProducts) { - setChosenStoreID(storeWithProducts.id) - setChosenProductID(storeWithProducts.onetimeProducts[0].id) + onSelectedBindingChange({ + storeID: storeWithProducts.id, + productID: storeWithProducts.onetimeProducts[0].id, + }) } else { - setChosenStoreID('') - setChosenProductID('') + onSelectedBindingChange({ storeID: '', productID: '' }) } } } setPhase('idle') }, - [productID, t] + [onSelectedBindingChange, productID, t] ) - const watchedMerchantID = form.watch('WaffoPancakeMerchantID') || '' - const watchedPrivateKey = form.watch('WaffoPancakePrivateKey') || '' + const watchedMerchantID = values.WaffoPancakeMerchantID || '' + const watchedPrivateKey = values.WaffoPancakePrivateKey || '' React.useEffect(() => { const m = watchedMerchantID.trim() const k = watchedPrivateKey.trim() @@ -272,20 +243,23 @@ export function WaffoPancakeSettingsSection(props: Props) { const initialLoadRef = React.useRef(false) React.useEffect(() => { if (initialLoadRef.current) return - if (!props.defaultValues.WaffoPancakeMerchantID.trim()) return + if (!defaultValues.WaffoPancakeMerchantID.trim()) return initialLoadRef.current = true - setPhase('verifying') - void verifyAndFetchCatalog('', '') - }, [props.defaultValues.WaffoPancakeMerchantID, verifyAndFetchCatalog]) + const timer = window.setTimeout(() => { + setPhase('verifying') + void verifyAndFetchCatalog('', '') + }, 0) + return () => window.clearTimeout(timer) + }, [defaultValues.WaffoPancakeMerchantID, verifyAndFetchCatalog]) // Returns typed creds when the operator edited either field; otherwise // blanks so the backend falls back to persisted creds. Without this, // returning admins (saved merchant ID but empty key field) would send // a mixed-state body that the backend rejects. const readCreds = () => { - const formMerchant = (form.getValues('WaffoPancakeMerchantID') || '').trim() - const formKey = (form.getValues('WaffoPancakePrivateKey') || '').trim() - const saved = (props.defaultValues.WaffoPancakeMerchantID || '').trim() + const formMerchant = (values.WaffoPancakeMerchantID || '').trim() + const formKey = (values.WaffoPancakePrivateKey || '').trim() + const saved = (defaultValues.WaffoPancakeMerchantID || '').trim() const edited = formMerchant !== saved || formKey.length > 0 if (!edited) return { merchantID: '', privateKey: '' } return { merchantID: formMerchant, privateKey: formKey } @@ -364,66 +338,12 @@ export function WaffoPancakeSettingsSection(props: Props) { } } - const handleSave = async () => { - // Sends raw form values (not readCreds): SaveWaffoPancakeConfig already - // treats a blank PrivateKey as "keep existing", and MerchantID stays - // populated from props for returning admins. - const merchantID = (form.getValues('WaffoPancakeMerchantID') || '').trim() - const privateKey = (form.getValues('WaffoPancakePrivateKey') || '').trim() - if (!merchantID) { - toast.error(t('Merchant ID is required')) - return - } - if (!chosenStoreID || !chosenProductID) { - toast.error(t('Pick or create both a store and a product before saving.')) - return - } - setPhase('saving') - try { - const body = await saveWaffoPancakeConfig({ - merchantID, - privateKey, - returnURL: removeTrailingSlash(returnURL.trim()), - storeID: chosenStoreID, - productID: chosenProductID, - }) - if ( - body?.message === 'success' && - typeof body.data === 'object' && - body.data - ) { - const saved = body.data as { product_id: string; store_id: string } - setStoreID(saved.store_id) - setProductID(saved.product_id) - toast.success(t('Waffo Pancake settings saved')) - } else { - const reason = typeof body?.data === 'string' ? body.data : undefined - toast.error( - reason - ? `${t('Waffo Pancake save failed')}: ${reason}` - : t('Waffo Pancake save failed') - ) - } - } catch (err) { - toast.error( - `${t('Waffo Pancake save failed')}: ${ - err instanceof Error ? err.message : String(err) - }` - ) - } finally { - setPhase('idle') - } - } - const verifying = phase === 'verifying' - const saving = phase === 'saving' // "Not edited" = MerchantID unchanged AND PrivateKey field blank, in // which case the backend falls back to persisted creds. Otherwise we // require both fields filled (mixed states would fail signature check). - const savedMerchantID = ( - props.defaultValues.WaffoPancakeMerchantID || '' - ).trim() + const savedMerchantID = (defaultValues.WaffoPancakeMerchantID || '').trim() const formMerchantID = watchedMerchantID.trim() const formPrivateKey = watchedPrivateKey.trim() const credsEdited = @@ -453,16 +373,6 @@ export function WaffoPancakeSettingsSection(props: Props) { return (
- - -

{t('Waffo Pancake MoR')}

@@ -471,93 +381,74 @@ export function WaffoPancakeSettingsSection(props: Props) { )}

-
- e.preventDefault()} - className='gap-y-4' - data-no-autosubmit='true' - > - {/* Blue box — webhook configuration only. */} -
-

{t('Webhook Configuration:')}

-
    -
  • - {t('Webhook URL (Test):')}{' '} - - {'/api/waffo-pancake/webhook/test'} - -
  • -
  • - {t('Webhook URL (Production):')}{' '} - - {'/api/waffo-pancake/webhook/prod'} - -
  • -
  • - {t( - 'Register each URL into the matching Test Mode / Production Mode webhook slot in the Pancake dashboard. Separate endpoints prevent test traffic from accidentally crediting production accounts.' - )} -
  • -
  • - {t('Configure at:')}{' '} - - {t('Waffo Pancake Dashboard')} - -
  • -
-
+
+ {/* Blue box — webhook configuration only. */} +
+

{t('Webhook Configuration:')}

+
    +
  • + {t('Webhook URL (Test):')}{' '} + + {'/api/waffo-pancake/webhook/test'} + +
  • +
  • + {t('Webhook URL (Production):')}{' '} + + {'/api/waffo-pancake/webhook/prod'} + +
  • +
  • + {t( + 'Register each URL into the matching Test Mode / Production Mode webhook slot in the Pancake dashboard. Separate endpoints prevent test traffic from accidentally crediting production accounts.' + )} +
  • +
  • + {t('Configure at:')}{' '} + + {t('Waffo Pancake Dashboard')} + +
  • +
+
- ( - - {t('Merchant ID')} - - field.onChange(event.target.value)} - /> - - - - )} +
+ + + onValueChange('WaffoPancakeMerchantID', event.target.value) + } /> +
- ( - - {t('API Private Key')} - -