Midtrans-Middleware/src/features/payments/components/CStorePanel.tsx

131 lines
5.5 KiB
TypeScript

import { Button } from '../../../components/ui/button'
import { usePaymentNavigation } from '../lib/navigation'
import React from 'react'
import { PaymentInstructions } from './PaymentInstructions'
import { TrustStrip } from './TrustStrip'
import { postCharge } from '../../../services/api'
import { InlinePaymentStatus } from './InlinePaymentStatus'
type StoreKey = 'alfamart' | 'indomaret'
// Shared guards/tasks to prevent duplicate charges under StrictMode/HMR and double clicks
const attemptedCStoreKeys = new Set<string>()
const cstoreTasks = new Map<string, Promise<any>>()
export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaultStore }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void; defaultStore?: StoreKey }) {
const nav = usePaymentNavigation()
const [selected] = React.useState<StoreKey | null>(defaultStore ?? null)
const [showGuide, setShowGuide] = React.useState(false)
const [busy, setBusy] = React.useState(false)
const [paymentCode, setPaymentCode] = React.useState('')
const [storeFromRes, setStoreFromRes] = React.useState('')
React.useEffect(() => {
let cancelled = false
async function run() {
// Only auto-charge when a store is selected, not locked, and code not yet generated
if (!selected || locked || paymentCode) return
const chargeKey = `${orderId}:${selected}`
// If there's already an in-flight task, await it
if (attemptedCStoreKeys.has(chargeKey) && cstoreTasks.has(chargeKey)) {
setBusy(true)
try {
const res = await cstoreTasks.get(chargeKey)!
if (!cancelled) {
if (typeof res?.payment_code === 'string') setPaymentCode(res.payment_code)
if (typeof res?.store === 'string') setStoreFromRes(res.store)
}
} catch (e) {
if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
} finally {
if (!cancelled) setBusy(false)
cstoreTasks.delete(chargeKey)
}
return
}
setBusy(true)
onChargeInitiated?.()
try {
const payload: Record<string, any> = {
payment_type: 'cstore',
transaction_details: { order_id: orderId, gross_amount: amount },
cstore: { store: selected, message: `Pembayaran untuk order ${orderId}` },
}
attemptedCStoreKeys.add(chargeKey)
const task = postCharge(payload)
cstoreTasks.set(chargeKey, task)
const res = await task
if (!cancelled) {
if (typeof res?.payment_code === 'string') setPaymentCode(res.payment_code)
if (typeof res?.store === 'string') setStoreFromRes(res.store)
}
} catch (e) {
if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
attemptedCStoreKeys.delete(chargeKey)
} finally {
if (!cancelled) setBusy(false)
cstoreTasks.delete(chargeKey)
}
}
run()
return () => { cancelled = true }
}, [selected, orderId, amount])
function copy(text: string, label: string) {
if (!text) return
navigator.clipboard?.writeText(text)
alert(`${label} disalin: ${text}`)
}
return (
<div className="space-y-3">
<div className="font-medium">Convenience Store</div>
{selected && (
<div className="text-xs text-black/60 dark:text-white/60">Toko dipilih: <span className="font-medium text-black dark:text-white">{selected.toUpperCase()}</span></div>
)}
<button
type="button"
onClick={() => setShowGuide((v) => !v)}
className="text-xs text-brand-600 hover:underline"
aria-expanded={showGuide}
>
Cara bayar
</button>
{showGuide && <PaymentInstructions method="cstore" />}
{locked && (
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.</div>
)}
<div className="pt-2 space-y-2">
<div className="rounded border border-black/10 dark:border-white/10 p-2 text-sm" aria-live="polite">
<div className="font-medium">Kode Pembayaran</div>
{!selected && (
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko terlebih dahulu di langkah sebelumnya.</div>
)}
{selected && busy && (
<div className="inline-flex items-center gap-2 text-xs text-black/60 dark:text-white/60">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
Membuat kode
</div>
)}
{selected && !busy && (storeFromRes || paymentCode) && (
<>
{storeFromRes ? <div>Toko: {storeFromRes.toUpperCase()}</div> : null}
{paymentCode ? <div>Kode: <span className="font-mono text-lg tracking-[0.06em] select-all">{paymentCode}</span></div> : null}
</>
)}
<div className="mt-2"><Button variant="outline" className="w-full sm:w-auto" onClick={() => copy(paymentCode, 'Kode pembayaran')} disabled={!paymentCode || busy}>Copy Kode</Button></div>
</div>
<Button
variant="outline"
className="w-full"
disabled={busy || (!locked && !paymentCode)}
onClick={() => nav.toStatus(orderId, 'cstore')}
>
Buka halaman status
</Button>
<InlinePaymentStatus orderId={orderId} method="cstore" />
</div>
<TrustStrip />
</div>
)
}