import { Button } from '../../../components/ui/button' import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from '../components/PaymentInstructions' import { GoPayLogosRow } from '../components/PaymentLogos' import { postCharge } from '../../../services/api' import { InlinePaymentStatus } from '../components/InlinePaymentStatus' import { toast } from '../../../components/ui/toast' import { LoadingOverlay } from '../../../components/LoadingOverlay' import { Alert } from '../../../components/alert/Alert' import { getErrorRecoveryAction, mapErrorToUserMessage } from '../../../lib/errorMessages' // Global guards/tasks to stabilize QR generation across StrictMode remounts const attemptedChargeKeys = new Set() const chargeTasks = new Map>() function sanitizeUrl(u?: string) { return (u || '').replace(/[`\s]+$/g, '').replace(/^\s+|\s+$/g, '').replace(/`/g, '') } function pickQrImageUrl(res: any, acts: Array<{ name?: string; method?: string; url: string }>) { const byName = acts.find((a) => /qr/i.test(a.name ?? '') || /qr-code/i.test(a.name ?? '')) const candidateFromActions = sanitizeUrl(byName?.url) if (candidateFromActions) return candidateFromActions const imageUrl = typeof res?.image_url === 'string' ? sanitizeUrl(res.image_url) : (typeof res?.qr_url === 'string' ? sanitizeUrl(res.qr_url) : '') if (imageUrl) return imageUrl const qrString = typeof res?.qr_string === 'string' ? res.qr_string : '' if (qrString) return `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrString)}` return '' } // export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) { const nav = usePaymentNavigation() const [busy, setBusy] = React.useState(false) const [qrUrl, setQrUrl] = React.useState('') const [actions, setActions] = React.useState>([]) const [mode, setMode] = React.useState<'gopay' | 'qris'>('qris') const lastChargeKeyRef = React.useRef('') const chargingKeyRef = React.useRef('') const [errorMessage, setErrorMessage] = React.useState('') const [recovery, setRecovery] = React.useState<'retry' | 'view-existing' | 'back'>('retry') function openGoPay() { const deeplink = actions.find((a) => (a.name ?? '').toLowerCase().includes('deeplink')) window.open(deeplink?.url || 'https://www.gojek.com/gopay/', '_blank') } function downloadQR() { if (qrUrl) { const a = document.createElement('a') a.href = qrUrl a.download = `QR-${orderId}.png` a.target = '_blank' a.click() } else { toast.error('QR belum tersedia. Klik "Buat QR" terlebih dulu.') } } return ( <>
GoPay / QRIS
{errorMessage && ( {errorMessage} {recovery === 'view-existing' && (
)}
)}
Mode:
Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}
{mode === 'qris' && (!qrUrl || busy) ? ( Membuat QR… ) : qrUrl ? ( QR untuk pembayaran ) : null}
Mode: {mode.toUpperCase()}
{locked && (
Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.
)}
) } // Auto-generate QR for QRIS when mode is set to 'qris' // Use effect to trigger charge once per order+mode, with guards for StrictMode/HMR export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy, setQrUrl, setActions, onChargeInitiated, lastChargeKeyRef, chargingKeyRef, setErrorMessage, setRecovery }: { orderId: string; amount: number; locked?: boolean; mode: 'gopay' | 'qris'; setBusy: (b: boolean) => void; setQrUrl: (u: string) => void; setActions: (a: Array<{ name?: string; method?: string; url: string }>) => void; onChargeInitiated?: () => void; lastChargeKeyRef: React.MutableRefObject; chargingKeyRef: React.MutableRefObject; setErrorMessage: (m: string) => void; setRecovery: (a: 'retry' | 'view-existing' | 'back') => void }) { React.useEffect(() => { let cancelled = false async function run() { if (mode !== 'qris' || locked) return const chargeKey = `${orderId}:qris` // If a QRIS charge is already in-flight (StrictMode remount), await the shared task if (attemptedChargeKeys.has(chargeKey) && chargeTasks.has(chargeKey)) { setBusy(true) chargingKeyRef.current = chargeKey try { const res = await chargeTasks.get(chargeKey)! const acts: Array<{ name?: string; method?: string; url: string }> = Array.isArray(res?.actions) ? res.actions : [] if (!cancelled) { setActions(acts) const url = pickQrImageUrl(res, acts) if (url) setQrUrl(url) try { const raw = localStorage.getItem('qrisCache') const map = raw ? (JSON.parse(raw) as Record }>) : {} map[orderId] = { url, actions: acts } localStorage.setItem('qrisCache', JSON.stringify(map)) } catch { void 0 } lastChargeKeyRef.current = chargeKey if (chargingKeyRef.current === chargeKey) chargingKeyRef.current = '' onChargeInitiated?.() } } catch (e) { if (!cancelled) { const msg = mapErrorToUserMessage(e) const act = getErrorRecoveryAction(e) setErrorMessage(msg) setRecovery(act) toast.error(msg) } } finally { if (!cancelled) { setBusy(false) if (chargingKeyRef.current === chargeKey) chargingKeyRef.current = '' } chargeTasks.delete(chargeKey) } return } if (lastChargeKeyRef.current === chargeKey || chargingKeyRef.current === chargeKey) return setBusy(true) chargingKeyRef.current = chargeKey try { const payload = { payment_type: 'qris', transaction_details: { order_id: orderId, gross_amount: amount }, qris: {} } attemptedChargeKeys.add(chargeKey) const task = postCharge(payload) chargeTasks.set(chargeKey, task) const res = await task const acts: Array<{ name?: string; method?: string; url: string }> = Array.isArray(res?.actions) ? res.actions : [] if (!cancelled) { setActions(acts) const url = pickQrImageUrl(res, acts) if (url) setQrUrl(url) try { const raw = localStorage.getItem('qrisCache') const map = raw ? (JSON.parse(raw) as Record }>) : {} map[orderId] = { url, actions: acts } localStorage.setItem('qrisCache', JSON.stringify(map)) } catch { void 0 } lastChargeKeyRef.current = chargeKey if (chargingKeyRef.current === chargeKey) chargingKeyRef.current = '' onChargeInitiated?.() } } catch (e) { if (!cancelled) { const msg = mapErrorToUserMessage(e) const act = getErrorRecoveryAction(e) setErrorMessage(msg) setRecovery(act) toast.error(msg) } attemptedChargeKeys.delete(chargeKey) } finally { if (!cancelled) { setBusy(false) if (chargingKeyRef.current === chargeKey) chargingKeyRef.current = '' } chargeTasks.delete(chargeKey) } } run() return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode]) return null }