Compare commits
2 Commits
f4e0ca4741
...
e4c81dce78
| Author | SHA1 | Date |
|---|---|---|
|
|
e4c81dce78 | |
|
|
e1f989447b |
|
|
@ -1,9 +1,10 @@
|
||||||
import { Link, Outlet } from 'react-router-dom'
|
import { Link, Outlet } from 'react-router-dom'
|
||||||
|
import { ToastHost } from '../components/ui/toast'
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<header className="border-b border-black/10 dark:border-white/20 bg-white dark:bg-black">
|
{/* <header className="border-b border-black/10 dark:border-white/20 bg-white dark:bg-black">
|
||||||
<div className="mx-auto max-w-5xl px-4 py-3 flex items-center justify-between">
|
<div className="mx-auto max-w-5xl px-4 py-3 flex items-center justify-between">
|
||||||
<Link to="/checkout" className="font-semibold text-brand-600">Core Midtrans CIFO</Link>
|
<Link to="/checkout" className="font-semibold text-brand-600">Core Midtrans CIFO</Link>
|
||||||
<nav className="flex gap-4 text-sm">
|
<nav className="flex gap-4 text-sm">
|
||||||
|
|
@ -11,13 +12,14 @@ export function AppLayout() {
|
||||||
<Link to="/history" className="hover:underline">History</Link>
|
<Link to="/history" className="hover:underline">History</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header> */}
|
||||||
<main className="mx-auto max-w-5xl px-4 py-6">
|
<main className="mx-auto max-w-5xl px-4 py-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<footer className="mx-auto max-w-5xl px-4 pb-8 text-xs text-black/60 dark:text-white/60">
|
<ToastHost />
|
||||||
|
{/* <footer className="mx-auto max-w-5xl px-4 pb-8 text-xs text-black/60 dark:text-white/60">
|
||||||
Brand: Merah–Hitam–Putih • Sandbox UI skeleton
|
Brand: Merah–Hitam–Putih • Sandbox UI skeleton
|
||||||
</footer>
|
</footer> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ export function Alert({ title, children }: { title: string; children?: React.Rea
|
||||||
<div className="text-brand-600">⚑</div>
|
<div className="text-brand-600">⚑</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold">{title}</div>
|
<div className="font-semibold">{title}</div>
|
||||||
{children ? <div className="text-sm text-black/70 dark:text-white/70">{children}</div> : null}
|
{children ? <div className="text-sm text-black/70">{children}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { cn } from '../../lib/cn'
|
import { cn } from '../../lib/cn'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-black disabled:opacity-50 disabled:pointer-events-none',
|
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:opacity-50 disabled:pointer-events-none',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary: 'bg-brand-600 text-white hover:bg-brand-700',
|
primary: 'bg-brand-600 text-white hover:bg-brand-700',
|
||||||
secondary: 'bg-white text-black hover:bg-white/90 dark:bg-black dark:text-white dark:hover:bg-black/90',
|
secondary: 'bg-white text-black border border-black/10 hover:bg-black/5',
|
||||||
outline: 'border border-black text-black hover:bg-black/5 dark:border-white dark:text-white dark:hover:bg-white/10',
|
outline: 'border border-black text-black hover:bg-black/5',
|
||||||
},
|
},
|
||||||
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-6' },
|
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-6' },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type ToastKind = 'info' | 'success' | 'error'
|
||||||
|
type ToastItem = { id: number; message: string; kind: ToastKind }
|
||||||
|
|
||||||
|
let nextId = 1
|
||||||
|
const subs = new Set<(t: ToastItem) => void>()
|
||||||
|
|
||||||
|
export const toast = {
|
||||||
|
show(message: string, kind: ToastKind = 'info') {
|
||||||
|
const item: ToastItem = { id: nextId++, message, kind }
|
||||||
|
subs.forEach((fn) => fn(item))
|
||||||
|
},
|
||||||
|
info(message: string) { toast.show(message, 'info') },
|
||||||
|
success(message: string) { toast.show(message, 'success') },
|
||||||
|
error(message: string) { toast.show(message, 'error') },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToastHost() {
|
||||||
|
const [items, setItems] = React.useState<ToastItem[]>([])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function onPush(t: ToastItem) {
|
||||||
|
setItems((prev) => [...prev, t])
|
||||||
|
// auto-remove after 3.5s
|
||||||
|
setTimeout(() => {
|
||||||
|
setItems((prev) => prev.filter((x) => x.id !== t.id))
|
||||||
|
}, 3500)
|
||||||
|
}
|
||||||
|
subs.add(onPush)
|
||||||
|
return () => { subs.delete(onPush) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!items.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||||
|
{items.map((it) => (
|
||||||
|
<div
|
||||||
|
key={it.id}
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
className={
|
||||||
|
`min-w-[240px] max-w-[360px] rounded shadow-lg border p-3 text-sm ` +
|
||||||
|
(it.kind === 'success'
|
||||||
|
? 'bg-green-600 text-white border-green-600'
|
||||||
|
: it.kind === 'error'
|
||||||
|
? 'bg-red-600 text-white border-red-600'
|
||||||
|
: 'bg-black text-white border-black')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{it.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@ import { usePaymentNavigation } from '../lib/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PaymentInstructions } from './PaymentInstructions'
|
import { PaymentInstructions } from './PaymentInstructions'
|
||||||
import { BcaInstructionList } from './BcaInstructionList'
|
import { BcaInstructionList } from './BcaInstructionList'
|
||||||
import { TrustStrip } from './TrustStrip'
|
|
||||||
import { type BankKey } from './PaymentLogos'
|
import { type BankKey } from './PaymentLogos'
|
||||||
import { postCharge } from '../../../services/api'
|
import { postCharge } from '../../../services/api'
|
||||||
import { Alert } from '../../../components/alert/Alert'
|
import { Alert } from '../../../components/alert/Alert'
|
||||||
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
||||||
|
import { toast } from '../../../components/ui/toast'
|
||||||
|
|
||||||
// Global guard to prevent duplicate auto-charge across StrictMode double-mounts
|
// Global guard to prevent duplicate auto-charge across StrictMode double-mounts
|
||||||
const attemptedChargeKeys = new Set<string>()
|
const attemptedChargeKeys = new Set<string>()
|
||||||
|
|
@ -29,7 +29,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
function copy(text: string, label: string) {
|
function copy(text: string, label: string) {
|
||||||
if (!text) return
|
if (!text) return
|
||||||
navigator.clipboard?.writeText(text)
|
navigator.clipboard?.writeText(text)
|
||||||
alert(`${label} disalin: ${text}`)
|
toast.success(`${label} disalin: ${text}`)
|
||||||
}
|
}
|
||||||
// Auto-create VA immediately when a bank is selected (runs once per selection)
|
// Auto-create VA immediately when a bank is selected (runs once per selection)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -125,7 +125,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
}
|
}
|
||||||
run()
|
run()
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selected])
|
}, [selected])
|
||||||
|
|
||||||
// Auto-show instructions when BCA is selected to reduce confusion
|
// Auto-show instructions when BCA is selected to reduce confusion
|
||||||
|
|
@ -137,43 +137,43 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="font-medium">Transfer bank</div>
|
<div className="font-medium">Transfer Bank</div>
|
||||||
{selected && (
|
{selected && (
|
||||||
<div className="flex items-center gap-2 text-base">
|
<div className="flex items-center gap-2 text-base">
|
||||||
<span className="text-black/60 dark:text-white/60">Bank:</span>
|
<span className="text-black/60">Bank:</span>
|
||||||
<span className="text-black/80 dark:text-white/80 font-semibold">{selected.toUpperCase()}</span>
|
<span className="text-black/80 font-semibold">{selected.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-black/70 dark:text-white/70">VA dibuat otomatis sesuai bank pilihan Anda.</div>
|
<div className="text-sm text-black/70">VA dibuat otomatis sesuai bank pilihan Anda.</div>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert title="Gagal membuat VA">{errorMessage}</Alert>
|
<Alert title="Gagal membuat VA">{errorMessage}</Alert>
|
||||||
)}
|
)}
|
||||||
{selected && (
|
{selected && (
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20">
|
<div className="rounded-lg p-3 border-2 border-black/30">
|
||||||
<div className="text-sm font-medium mb-2">Virtual Account</div>
|
<div className="text-sm font-medium mb-2">Virtual Account</div>
|
||||||
<div className="text-sm text-black/70 dark:text-white/70">
|
<div className="text-sm text-black/70">
|
||||||
{vaCode ? (
|
{vaCode ? (
|
||||||
<span>
|
<span>
|
||||||
Nomor VA:
|
Nomor VA:
|
||||||
<span className="block break-all mt-1 font-mono text-xl sm:text-2xl md:text-3xl font-semibold tracking-normal text-black dark:text-white">{vaCode}</span>
|
<span className="block break-all mt-1 font-mono text-xl sm:text-2xl md:text-3xl font-semibold tracking-normal text-black">{vaCode}</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
|
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
|
||||||
{busy && <span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />}
|
{busy && <span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 border-t-transparent" aria-hidden />}
|
||||||
{busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
|
{busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{billKey && (
|
{billKey && (
|
||||||
<span className="ml-3">Bill Key: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billKey}</span></span>
|
<span className="ml-3">Bill Key: <span className="font-mono text-lg font-semibold text-black">{billKey}</span></span>
|
||||||
)}
|
)}
|
||||||
{billerCode && (
|
{billerCode && (
|
||||||
<span className="ml-3">Biller Code: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billerCode}</span></span>
|
<span className="ml-3">Biller Code: <span className="font-mono text-lg font-semibold text-black">{billerCode}</span></span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="mt-2 flex gap-2">
|
||||||
<Button variant="outline" size="sm" onClick={() => copy(vaCode, 'VA')} disabled={!vaCode}>Copy VA</Button>
|
<Button variant="secondary" size="sm" onClick={() => copy(vaCode, 'VA')} disabled={!vaCode}>Copy VA</Button>
|
||||||
<Button variant="outline" size="sm" onClick={() => copy(billKey, 'Bill Key')} disabled={!billKey}>Copy Bill Key</Button>
|
<Button variant="secondary" size="sm" onClick={() => copy(billKey, 'Bill Key')} disabled={!billKey}>Copy Bill Key</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -187,7 +187,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
{selected === 'bca' ? (
|
{selected === 'bca' ? (
|
||||||
<BcaInstructionList />
|
<BcaInstructionList />
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20">
|
<div className="rounded-lg border-2 border-black/30 p-3 bg-white">
|
||||||
<div className="text-sm font-medium mb-2">Instruksi pembayaran</div>
|
<div className="text-sm font-medium mb-2">Instruksi pembayaran</div>
|
||||||
<PaymentInstructions method="bank_transfer" />
|
<PaymentInstructions method="bank_transfer" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,7 +195,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.</div>
|
<div className="text-xs text-black/60">Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 space-y-2">
|
<div className="pt-2 space-y-2">
|
||||||
{(!vaCode || errorMessage) && (
|
{(!vaCode || errorMessage) && (
|
||||||
|
|
@ -280,7 +280,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 dark:border-black/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 border-t-transparent" aria-hidden />
|
||||||
Membuat VA…
|
Membuat VA…
|
||||||
</span>
|
</span>
|
||||||
) : 'Buat VA'}
|
) : 'Buat VA'}
|
||||||
|
|
@ -295,7 +295,6 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
Buka halaman status
|
Buka halaman status
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<TrustStrip />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ type TabKey = 'mobile' | 'atm' | 'ib'
|
||||||
export function BcaInstructionList() {
|
export function BcaInstructionList() {
|
||||||
const [tab, setTab] = React.useState<TabKey>('mobile')
|
const [tab, setTab] = React.useState<TabKey>('mobile')
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 rounded-lg border-2 border-black/20 dark:border-white/20 bg-white dark:bg-black/20">
|
<div className="mt-2 rounded-lg border-2 border-gray-300 bg-white">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="text-sm font-medium mb-2">Instruksi BCA</div>
|
<div className="text-sm font-medium mb-2">Instruksi BCA</div>
|
||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Metode pembayaran BCA"
|
aria-label="Metode pembayaran BCA"
|
||||||
className="inline-flex mb-3 rounded-md border border-black/20 dark:border-white/20 overflow-hidden"
|
className="inline-flex mb-3 rounded-md border border-gray-300 overflow-hidden"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -20,7 +20,7 @@ export function BcaInstructionList() {
|
||||||
aria-selected={tab === 'mobile'}
|
aria-selected={tab === 'mobile'}
|
||||||
aria-controls="panel-bca-mobile"
|
aria-controls="panel-bca-mobile"
|
||||||
onClick={() => setTab('mobile')}
|
onClick={() => setTab('mobile')}
|
||||||
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'mobile' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
className={`px-3 py-2 text-xs font-medium focus:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${tab === 'mobile' ? 'bg-black text-white' : 'bg-transparent hover:bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
BCA Mobile
|
BCA Mobile
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -31,7 +31,7 @@ export function BcaInstructionList() {
|
||||||
aria-selected={tab === 'atm'}
|
aria-selected={tab === 'atm'}
|
||||||
aria-controls="panel-bca-atm"
|
aria-controls="panel-bca-atm"
|
||||||
onClick={() => setTab('atm')}
|
onClick={() => setTab('atm')}
|
||||||
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'atm' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
className={`px-3 py-2 text-xs font-medium focus:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${tab === 'atm' ? 'bg-black text-white' : 'bg-transparent hover:bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
ATM BCA
|
ATM BCA
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -42,7 +42,7 @@ export function BcaInstructionList() {
|
||||||
aria-selected={tab === 'ib'}
|
aria-selected={tab === 'ib'}
|
||||||
aria-controls="panel-bca-ib"
|
aria-controls="panel-bca-ib"
|
||||||
onClick={() => setTab('ib')}
|
onClick={() => setTab('ib')}
|
||||||
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'ib' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
className={`px-3 py-2 text-xs font-medium focus:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${tab === 'ib' ? 'bg-black text-white' : 'bg-transparent hover:bg-gray-100'}`}
|
||||||
>
|
>
|
||||||
KlikBCA
|
KlikBCA
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -63,7 +63,7 @@ export function BcaInstructionList() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-black/10 dark:border-white/10 p-3 text-xs text-black/60 dark:text-white/60">
|
<div className="border-t border-gray-200 p-3 text-xs text-gray-600">
|
||||||
Catatan: Gunakan label "Nomor VA" secara konsisten di semua metode.
|
Catatan: Gunakan label "Nomor VA" secara konsisten di semua metode.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -111,7 +111,7 @@ function StepsKlikBca() {
|
||||||
|
|
||||||
function StepList({ steps }: { steps: string[] }) {
|
function StepList({ steps }: { steps: string[] }) {
|
||||||
return (
|
return (
|
||||||
<ol className="list-decimal list-inside space-y-2 text-sm text-black/80 dark:text-white/80">
|
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-800">
|
||||||
{steps.map((s, i) => (
|
{steps.map((s, i) => (
|
||||||
<li key={i}>{s}</li>
|
<li key={i}>{s}</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Button } from '../../../components/ui/button'
|
import { Button } from '../../../components/ui/button'
|
||||||
|
import { toast } from '../../../components/ui/toast'
|
||||||
import { usePaymentNavigation } from '../lib/navigation'
|
import { usePaymentNavigation } from '../lib/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PaymentInstructions } from './PaymentInstructions'
|
import { PaymentInstructions } from './PaymentInstructions'
|
||||||
import { TrustStrip } from './TrustStrip'
|
|
||||||
import { postCharge } from '../../../services/api'
|
import { postCharge } from '../../../services/api'
|
||||||
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
if (typeof res?.store === 'string') setStoreFromRes(res.store)
|
if (typeof res?.store === 'string') setStoreFromRes(res.store)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
|
if (!cancelled) toast.error(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setBusy(false)
|
if (!cancelled) setBusy(false)
|
||||||
cstoreTasks.delete(chargeKey)
|
cstoreTasks.delete(chargeKey)
|
||||||
|
|
@ -60,7 +60,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
if (typeof res?.store === 'string') setStoreFromRes(res.store)
|
if (typeof res?.store === 'string') setStoreFromRes(res.store)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
|
if (!cancelled) toast.error(`Gagal membuat kode pembayaran: ${(e as Error).message}`)
|
||||||
attemptedCStoreKeys.delete(chargeKey)
|
attemptedCStoreKeys.delete(chargeKey)
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setBusy(false)
|
if (!cancelled) setBusy(false)
|
||||||
|
|
@ -74,14 +74,14 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
function copy(text: string, label: string) {
|
function copy(text: string, label: string) {
|
||||||
if (!text) return
|
if (!text) return
|
||||||
navigator.clipboard?.writeText(text)
|
navigator.clipboard?.writeText(text)
|
||||||
alert(`${label} disalin: ${text}`)
|
toast.success(`${label} disalin: ${text}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="font-medium">Convenience Store</div>
|
<div className="font-medium">Convenience Store</div>
|
||||||
{selected && (
|
{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>
|
<div className="text-xs text-gray-600">Toko dipilih: <span className="font-medium text-gray-900">{selected.toUpperCase()}</span></div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -93,17 +93,17 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
</button>
|
</button>
|
||||||
{showGuide && <PaymentInstructions method="cstore" />}
|
{showGuide && <PaymentInstructions method="cstore" />}
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.</div>
|
<div className="text-xs text-gray-600">Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 space-y-2">
|
<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="rounded border border-gray-300 p-2 text-sm" aria-live="polite">
|
||||||
<div className="font-medium">Kode Pembayaran</div>
|
<div className="font-medium">Kode Pembayaran</div>
|
||||||
{!selected && (
|
{!selected && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko terlebih dahulu di langkah sebelumnya.</div>
|
<div className="text-xs text-gray-600">Pilih toko terlebih dahulu di langkah sebelumnya.</div>
|
||||||
)}
|
)}
|
||||||
{selected && busy && (
|
{selected && busy && (
|
||||||
<div className="inline-flex items-center gap-2 text-xs text-black/60 dark:text-white/60">
|
<div className="inline-flex items-center gap-2 text-xs text-gray-600">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" aria-hidden />
|
||||||
Membuat kode…
|
Membuat kode…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -125,7 +125,6 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
</Button>
|
</Button>
|
||||||
<InlinePaymentStatus orderId={orderId} method="cstore" />
|
<InlinePaymentStatus orderId={orderId} method="cstore" />
|
||||||
</div>
|
</div>
|
||||||
<TrustStrip />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,13 +2,13 @@ import { Button } from '../../../components/ui/button'
|
||||||
import { usePaymentNavigation } from '../lib/navigation'
|
import { usePaymentNavigation } from '../lib/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PaymentInstructions } from './PaymentInstructions'
|
import { PaymentInstructions } from './PaymentInstructions'
|
||||||
import { TrustStrip } from './TrustStrip'
|
|
||||||
import { CardLogosRow } from './PaymentLogos'
|
import { CardLogosRow } from './PaymentLogos'
|
||||||
import { ensureMidtrans3ds, getCardToken, authenticate3ds } from '../lib/midtrans3ds'
|
import { ensureMidtrans3ds, getCardToken, authenticate3ds } from '../lib/midtrans3ds'
|
||||||
import { Logger } from '../../../lib/logger'
|
import { Logger } from '../../../lib/logger'
|
||||||
import { Env } from '../../../lib/env'
|
import { Env } from '../../../lib/env'
|
||||||
import { postCharge } from '../../../services/api'
|
import { postCharge } from '../../../services/api'
|
||||||
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
||||||
|
import { toast } from '../../../components/ui/toast'
|
||||||
|
|
||||||
export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) {
|
export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) {
|
||||||
const nav = usePaymentNavigation()
|
const nav = usePaymentNavigation()
|
||||||
|
|
@ -41,16 +41,16 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
<CardLogosRow compact />
|
<CardLogosRow compact />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Nomor kartu</div>
|
<div className="text-xs text-gray-600">Nomor kartu</div>
|
||||||
<input type="text" inputMode="numeric" maxLength={23} placeholder="0000-0000-0000-0000" className="w-full rounded border px-3 py-2" value={cardNumber} onChange={(e) => setCardNumber(formatCard(e.target.value))} />
|
<input type="text" inputMode="numeric" maxLength={23} placeholder="0000-0000-0000-0000" className="w-full rounded border px-3 py-2" value={cardNumber} onChange={(e) => setCardNumber(formatCard(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Masa berlaku</div>
|
<div className="text-xs text-gray-600">Masa berlaku</div>
|
||||||
<input type="text" inputMode="numeric" maxLength={5} placeholder="MM/YY" className="w-full rounded border px-3 py-2" value={exp} onChange={(e) => setExp(formatExp(e.target.value))} />
|
<input type="text" inputMode="numeric" maxLength={5} placeholder="MM/YY" className="w-full rounded border px-3 py-2" value={exp} onChange={(e) => setExp(formatExp(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">CVV</div>
|
<div className="text-xs text-gray-600">CVV</div>
|
||||||
<input type="password" inputMode="numeric" maxLength={4} placeholder="123" className="w-full rounded border px-3 py-2" value={cvv} onChange={(e) => setCvv(formatCvv(e.target.value))} />
|
<input type="password" inputMode="numeric" maxLength={4} placeholder="123" className="w-full rounded border px-3 py-2" value={cvv} onChange={(e) => setCvv(formatCvv(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,7 +69,7 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
</button>
|
</button>
|
||||||
{showGuide && <PaymentInstructions method="credit_card" />}
|
{showGuide && <PaymentInstructions method="credit_card" />}
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Lanjutkan verifikasi 3DS/OTP pada kartu Anda.</div>
|
<div className="text-xs text-gray-600">Metode terkunci. Lanjutkan verifikasi 3DS/OTP pada kartu Anda.</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 space-y-2">
|
<div className="pt-2 space-y-2">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -89,24 +89,24 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
// Minimal validation
|
// Minimal validation
|
||||||
if (sanitizedCard.length < 13 || sanitizedCard.length > 19) {
|
if (sanitizedCard.length < 13 || sanitizedCard.length > 19) {
|
||||||
Logger.warn('card.input.invalid', { reason: 'card_number_length', length: sanitizedCard.length })
|
Logger.warn('card.input.invalid', { reason: 'card_number_length', length: sanitizedCard.length })
|
||||||
alert('Nomor kartu tidak valid. Harus 13–19 digit.')
|
toast.error('Nomor kartu tidak valid. Harus 13–19 digit.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const mmNum = Number(mm)
|
const mmNum = Number(mm)
|
||||||
if (!mm || mm.length !== 2 || mmNum < 1 || mmNum > 12) {
|
if (!mm || mm.length !== 2 || mmNum < 1 || mmNum > 12) {
|
||||||
Logger.warn('card.input.invalid', { reason: 'exp_month', value: mm })
|
Logger.warn('card.input.invalid', { reason: 'exp_month', value: mm })
|
||||||
alert('Format bulan kedaluwarsa tidak valid (MM).')
|
toast.error('Format bulan kedaluwarsa tidak valid (MM).')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!yy || yy.length < 2) {
|
if (!yy || yy.length < 2) {
|
||||||
Logger.warn('card.input.invalid', { reason: 'exp_year', value: yy })
|
Logger.warn('card.input.invalid', { reason: 'exp_year', value: yy })
|
||||||
alert('Format tahun kedaluwarsa tidak valid (YY).')
|
toast.error('Format tahun kedaluwarsa tidak valid (YY).')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cvvSan = cvv.replace(/[^0-9]/g, '')
|
const cvvSan = cvv.replace(/[^0-9]/g, '')
|
||||||
if (cvvSan.length < 3 || cvvSan.length > 4) {
|
if (cvvSan.length < 3 || cvvSan.length > 4) {
|
||||||
Logger.warn('card.input.invalid', { reason: 'cvv_length', length: cvvSan.length })
|
Logger.warn('card.input.invalid', { reason: 'cvv_length', length: cvvSan.length })
|
||||||
alert('CVV harus 3–4 digit.')
|
toast.error('CVV harus 3–4 digit.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Env.LOG_LEVEL === 'debug') {
|
if (Env.LOG_LEVEL === 'debug') {
|
||||||
|
|
@ -132,7 +132,7 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('card.process.error', { message: (e as Error)?.message })
|
Logger.error('card.process.error', { message: (e as Error)?.message })
|
||||||
alert(`Gagal memproses kartu: ${(e as Error).message}`)
|
toast.error(`Gagal memproses kartu: ${(e as Error).message}`)
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2">
|
<span className="inline-flex items-center justify-center gap-2">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 dark:border-black/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 border-t-transparent" aria-hidden />
|
||||||
Memproses pembayaran…
|
Memproses pembayaran…
|
||||||
</span>
|
</span>
|
||||||
) : 'Bayar sekarang'}
|
) : 'Bayar sekarang'}
|
||||||
|
|
@ -156,7 +156,6 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
|
||||||
</div>
|
</div>
|
||||||
{/* Status inline dengan polling otomatis */}
|
{/* Status inline dengan polling otomatis */}
|
||||||
<InlinePaymentStatus orderId={orderId} method="credit_card" compact />
|
<InlinePaymentStatus orderId={orderId} method="credit_card" compact />
|
||||||
<TrustStrip />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,10 @@ import { Button } from '../../../components/ui/button'
|
||||||
import { usePaymentNavigation } from '../lib/navigation'
|
import { usePaymentNavigation } from '../lib/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PaymentInstructions } from './PaymentInstructions'
|
import { PaymentInstructions } from './PaymentInstructions'
|
||||||
import { TrustStrip } from './TrustStrip'
|
|
||||||
import { GoPayLogosRow } from './PaymentLogos'
|
import { GoPayLogosRow } from './PaymentLogos'
|
||||||
import { postCharge } from '../../../services/api'
|
import { postCharge } from '../../../services/api'
|
||||||
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
import { InlinePaymentStatus } from './InlinePaymentStatus'
|
||||||
|
import { toast } from '../../../components/ui/toast'
|
||||||
|
|
||||||
// Global guards/tasks to stabilize QR generation across StrictMode remounts
|
// Global guards/tasks to stabilize QR generation across StrictMode remounts
|
||||||
const attemptedChargeKeys = new Set<string>()
|
const attemptedChargeKeys = new Set<string>()
|
||||||
|
|
@ -48,7 +48,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
a.target = '_blank'
|
a.target = '_blank'
|
||||||
a.click()
|
a.click()
|
||||||
} else {
|
} else {
|
||||||
alert('QR belum tersedia. Klik "Buat QR" terlebih dulu.')
|
toast.error('QR belum tersedia. Klik "Buat QR" terlebih dulu.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
@ -68,13 +68,13 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
<div className="font-medium">GoPay / QRIS</div>
|
<div className="font-medium">GoPay / QRIS</div>
|
||||||
<GoPayLogosRow compact />
|
<GoPayLogosRow compact />
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-black/60 dark:text-white/60">Mode:</span>
|
<span className="text-black/60">Mode:</span>
|
||||||
<div className="inline-flex rounded-md border-2 border-black/20 dark:border-white/20 overflow-hidden" role="group" aria-label="Pilih mode pembayaran">
|
<div className="inline-flex rounded-md border-2 border-black/20 overflow-hidden" role="group" aria-label="Pilih mode pembayaran">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMode('gopay')}
|
onClick={() => setMode('gopay')}
|
||||||
aria-pressed={mode==='gopay'}
|
aria-pressed={mode==='gopay'}
|
||||||
className={`px-2 py-1 focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${mode==='gopay' ? 'bg-black text-white' : 'bg-white dark:bg-black text-black dark:text-white hover:bg-black/10 dark:hover:bg-white/10'}`}
|
className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode==='gopay' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
|
||||||
>
|
>
|
||||||
GoPay
|
GoPay
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -82,25 +82,25 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMode('qris')}
|
onClick={() => setMode('qris')}
|
||||||
aria-pressed={mode==='qris'}
|
aria-pressed={mode==='qris'}
|
||||||
className={`px-2 py-1 focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white dark:bg-black text-black dark:text-white hover:bg-black/10 dark:hover:bg-white/10'}`}
|
className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
|
||||||
>
|
>
|
||||||
QRIS
|
QRIS
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-3 flex flex-col items-center gap-2">
|
<div className="rounded border border-black/10 p-3 flex flex-col items-center gap-2">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}</div>
|
<div className="text-xs text-black/60">Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}</div>
|
||||||
<div className="relative w-full max-w-[280px] aspect-square grid place-items-center rounded-md border border-black/20 dark:border-white/20 bg-white">
|
<div className="relative w-full max-w-[280px] aspect-square grid place-items-center rounded-md border border-black/20 bg-white">
|
||||||
{mode === 'qris' && (!qrUrl || busy) ? (
|
{mode === 'qris' && (!qrUrl || busy) ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2 text-xs text-black/60 dark:text-white/60" role="status" aria-live="polite">
|
<span className="inline-flex items-center justify-center gap-2 text-xs text-black/60" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 border-t-transparent" aria-hidden />
|
||||||
Membuat QR…
|
Membuat QR…
|
||||||
</span>
|
</span>
|
||||||
) : qrUrl ? (
|
) : qrUrl ? (
|
||||||
<img src={qrUrl} alt="QR untuk pembayaran" className="aspect-square w-full max-w-[260px] mx-auto" />
|
<img src={qrUrl} alt="QR untuk pembayaran" className="aspect-square w-full max-w-[260px] mx-auto" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-black/50 dark:text-white/50">Mode: {mode.toUpperCase()}</div>
|
<div className="text-[10px] text-black/50">Mode: {mode.toUpperCase()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 w-full">
|
<div className="flex flex-col sm:flex-row gap-2 w-full">
|
||||||
<Button variant="outline" className="w-full sm:w-auto" onClick={downloadQR} disabled={!qrUrl}>Download QR</Button>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={downloadQR} disabled={!qrUrl}>Download QR</Button>
|
||||||
|
|
@ -125,7 +125,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.</div>
|
<div className="text-xs text-black/60">Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<InlinePaymentStatus orderId={orderId} method={mode} />
|
<InlinePaymentStatus orderId={orderId} method={mode} />
|
||||||
|
|
@ -139,14 +139,13 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 border-t-transparent" aria-hidden />
|
||||||
Menuju status…
|
Menuju status…
|
||||||
</span>
|
</span>
|
||||||
) : 'Buka halaman status'}
|
) : 'Buka halaman status'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TrustStrip />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +175,7 @@ export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy,
|
||||||
onChargeInitiated?.()
|
onChargeInitiated?.()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) alert(`Gagal membuat QR: ${(e as Error).message}`)
|
if (!cancelled) toast.error(`Gagal membuat QR: ${(e as Error).message}`)
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
|
|
@ -205,7 +204,7 @@ export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy,
|
||||||
onChargeInitiated?.()
|
onChargeInitiated?.()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) alert(`Gagal membuat QR: ${(e as Error).message}`)
|
if (!cancelled) toast.error(`Gagal membuat QR: ${(e as Error).message}`)
|
||||||
attemptedChargeKeys.delete(chargeKey)
|
attemptedChargeKeys.delete(chargeKey)
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
|
||||||
const isFailure = ['deny', 'cancel', 'expire', 'refund', 'chargeback'].includes(status)
|
const isFailure = ['deny', 'cancel', 'expire', 'refund', 'chargeback'].includes(status)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded border ${compact ? 'p-2' : 'p-3'} border-black/10 dark:border-white/10 bg-white dark:bg-black/20`} aria-live="polite">
|
<div className={`rounded border ${compact ? 'p-2' : 'p-3'} border-black/10 bg-white`} aria-live="polite">
|
||||||
{/* Header minimal tanpa detail teknis */}
|
{/* Header minimal tanpa detail teknis */}
|
||||||
<div className="text-sm font-medium">Status pembayaran</div>
|
<div className="text-sm font-medium">Status pembayaran</div>
|
||||||
|
|
||||||
|
|
@ -26,10 +26,10 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="mt-2 text-sm">
|
<div className="mt-2 text-sm">
|
||||||
<span className="inline-flex items-center gap-2" role="status">
|
<span className="inline-flex items-center gap-2" role="status">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 border-t-transparent" aria-hidden />
|
||||||
Mengecek pembayaran…
|
Mengecek pembayaran…
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1 text-[11px] text-black/60 dark:text-white/60">Kami memeriksa otomatis setiap 3 detik.</div>
|
<div className="mt-1 text-[11px] text-black/60">Kami memeriksa otomatis setiap 3 detik.</div>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="mt-2 text-sm text-brand-600">Gagal memuat status. Coba refresh.</div>
|
<div className="mt-2 text-sm text-brand-600">Gagal memuat status. Coba refresh.</div>
|
||||||
|
|
@ -45,9 +45,9 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
|
||||||
<div className="text-base font-semibold">Pembayaran berhasil</div>
|
<div className="text-base font-semibold">Pembayaran berhasil</div>
|
||||||
</div>
|
</div>
|
||||||
{data?.grossAmount ? (
|
{data?.grossAmount ? (
|
||||||
<div className="mt-1 text-sm text-black/70 dark:text-white/70">Total dibayar: {formatIDR(data.grossAmount)}</div>
|
<div className="mt-1 text-sm text-black/70">Total dibayar: {formatIDR(data.grossAmount)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">Terima kasih! Pesanan Anda sedang diproses.</div>
|
<div className="mt-1 text-xs text-black/60">Terima kasih! Pesanan Anda sedang diproses.</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<Button className="w-full sm:w-auto" onClick={() => nav.toHistory()}>Lihat riwayat pembayaran</Button>
|
<Button className="w-full sm:w-auto" onClick={() => nav.toHistory()}>Lihat riwayat pembayaran</Button>
|
||||||
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Kembali ke checkout</Button>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Kembali ke checkout</Button>
|
||||||
|
|
@ -66,7 +66,7 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
|
||||||
</span>
|
</span>
|
||||||
<div className="text-base font-semibold">Pembayaran belum berhasil</div>
|
<div className="text-base font-semibold">Pembayaran belum berhasil</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">Silakan coba lagi atau pilih metode lain.</div>
|
<div className="mt-1 text-xs text-black/60">Silakan coba lagi atau pilih metode lain.</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<Button className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Coba lagi</Button>
|
<Button className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Coba lagi</Button>
|
||||||
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toStatus(orderId, method)}>Lihat detail status</Button>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toStatus(orderId, method)}>Lihat detail status</Button>
|
||||||
|
|
@ -75,18 +75,18 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500/15 text-yellow-700 dark:text-yellow-300">
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500/15 text-yellow-700">
|
||||||
{/* hourglass/spinner icon */}
|
{/* hourglass/spinner icon */}
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-yellow-600/50 dark:border-yellow-300/80 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-yellow-600/50 border-t-transparent" aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
<div className="text-base font-semibold">Menunggu pembayaran</div>
|
<div className="text-base font-semibold">Menunggu pembayaran</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[11px] text-black/60 dark:text-white/60">Kami memeriksa otomatis setiap 3 detik sampai selesai.</div>
|
<div className="mt-1 text-[11px] text-black/60">Kami memeriksa otomatis setiap 3 detik sampai selesai.</div>
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
<Button variant="outline" className="w-full sm:w-auto" onClick={() => refetch()} aria-busy={isRefetching} disabled={isRefetching}>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={() => refetch()} aria-busy={isRefetching} disabled={isRefetching}>
|
||||||
{isRefetching ? (
|
{isRefetching ? (
|
||||||
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
|
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
|
||||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
|
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 border-t-transparent" aria-hidden />
|
||||||
Memuat…
|
Memuat…
|
||||||
</span>
|
</span>
|
||||||
) : 'Refresh sekarang'}
|
) : 'Refresh sekarang'}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ export function PaymentInstructions({ method, title, steps }: { method?: Payment
|
||||||
const finalSteps = computed.length ? computed : ['Ikuti instruksi yang muncul pada layar pembayaran.']
|
const finalSteps = computed.length ? computed : ['Ikuti instruksi yang muncul pada layar pembayaran.']
|
||||||
const finalTitle = title ?? 'Cara bayar'
|
const finalTitle = title ?? 'Cara bayar'
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20" role="region" aria-label={finalTitle}>
|
<div className="mt-2 rounded-lg border-2 border-gray-300 p-3 bg-white" role="region" aria-label={finalTitle}>
|
||||||
<div className="text-sm font-medium mb-2">{finalTitle}</div>
|
<div className="text-sm font-medium mb-2">{finalTitle}</div>
|
||||||
<ol className="list-decimal list-inside space-y-2 text-sm text-black/80 dark:text-white/80">
|
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-800">
|
||||||
{finalSteps.map((s, i) => (
|
{finalSteps.map((s, i) => (
|
||||||
<li key={i}>{s}</li>
|
<li key={i}>{s}</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export interface PaymentMethodListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseItems: Array<{ key: PaymentMethod; title: string; subtitle: string; icon?: React.ReactNode }> = [
|
const baseItems: Array<{ key: PaymentMethod; title: string; subtitle: string; icon?: React.ReactNode }> = [
|
||||||
{ key: 'bank_transfer', title: 'Transfer bank', subtitle: 'BCA • BNI • BRI • CIMB • Mandiri • Permata', icon: <img src="/logos/logo-semua-bank.PNG" alt="Semua bank yang didukung" className="h-6 sm:h-8 object-contain" /> },
|
{ key: 'bank_transfer', title: 'Transfer Bank', subtitle: 'BCA • BNI • BRI • CIMB • Mandiri • Permata', icon: <img src="/logos/logo-semua-bank.PNG" alt="Semua bank yang didukung" className="h-6 sm:h-8 object-contain" /> },
|
||||||
{ key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: <CardLogosRow compact size="xs" /> },
|
{ key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: <CardLogosRow compact size="xs" /> },
|
||||||
{ key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: <GoPayLogosRow compact size="xs" /> },
|
{ key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: <GoPayLogosRow compact size="xs" /> },
|
||||||
{ key: 'cstore', title: 'Convenience Store', subtitle: '', icon: <CStoreLogosRow compact size="xs" /> },
|
{ key: 'cstore', title: 'Convenience Store', subtitle: '', icon: <CStoreLogosRow compact size="xs" /> },
|
||||||
|
|
@ -32,26 +32,26 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium">Metode pembayaran</div>
|
<div className="text-sm font-medium">Metode pembayaran</div>
|
||||||
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 divide-y-[2px] divide-black/20 dark:divide-white/20 bg-white dark:bg-black">
|
<div className="rounded-lg border-2 border-black/30 divide-y-[2px] divide-black/20 bg-white">
|
||||||
{items.map((it) => (
|
{items.map((it) => (
|
||||||
<div key={it.key}>
|
<div key={it.key}>
|
||||||
<button
|
<button
|
||||||
onClick={() => !disabled && onSelect(it.key)}
|
onClick={() => !disabled && onSelect(it.key)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full text-left p-4 min-h-[52px] flex items-center justify-between ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer hover:bg-black/10 dark:hover:bg-white/15'} ${selected === it.key ? 'bg-black/10 dark:bg-white/20' : ''} focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-black`}
|
className={`w-full text-left p-4 min-h-[52px] flex items-center justify-between ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer hover:bg-black/10'} ${selected === it.key ? 'bg-black/10' : ''} focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white`}
|
||||||
aria-pressed={selected === it.key}
|
aria-pressed={selected === it.key}
|
||||||
aria-expanded={selected === it.key}
|
aria-expanded={selected === it.key}
|
||||||
aria-controls={`panel-${it.key}`}
|
aria-controls={`panel-${it.key}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-base font-semibold text-black dark:text-white">{it.title}</div>
|
<div className="text-base font-semibold text-black">{it.title}</div>
|
||||||
{it.key === 'bank_transfer' && it.subtitle && (
|
{it.key === 'bank_transfer' && it.subtitle && (
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">
|
<div className="mt-1 text-xs text-black/60">
|
||||||
{it.subtitle}
|
{it.subtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{it.key === 'cpay' && it.subtitle && (
|
{it.key === 'cpay' && it.subtitle && (
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">
|
<div className="mt-1 text-xs text-black/60">
|
||||||
{it.subtitle}
|
{it.subtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -62,11 +62,11 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e
|
||||||
{it.icon}
|
{it.icon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span aria-hidden className={`text-black/60 dark:text-white/60 text-lg transition-transform ${selected === it.key ? 'rotate-90' : ''}`}>›</span>
|
<span aria-hidden className={`text-black/60 text-lg transition-transform ${selected === it.key ? 'rotate-90' : ''}`}>›</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{selected === it.key && renderPanel && (
|
{selected === it.key && renderPanel && (
|
||||||
<div id={`panel-${it.key}`} className="p-3 bg-white dark:bg-black/20">
|
<div id={`panel-${it.key}`} className="p-3 bg-white">
|
||||||
{renderPanel(it.key)}
|
{renderPanel(it.key)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export interface PaymentSheetProps {
|
||||||
showStatusCTA?: boolean
|
showStatusCTA?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) {
|
export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) {
|
||||||
const countdown = useCountdown(expireAt)
|
const countdown = useCountdown(expireAt)
|
||||||
const [expanded, setExpanded] = React.useState(true)
|
const [expanded, setExpanded] = React.useState(true)
|
||||||
return (
|
return (
|
||||||
|
|
@ -38,7 +38,7 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
|
||||||
<div className="bg-[#0c1f3f] text-white p-3 sm:p-4 flex items-center justify-between">
|
<div className="bg-[#0c1f3f] text-white p-3 sm:p-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<div className="rounded bg-white text-black px-2 py-1 text-[11px] sm:text-xs font-bold" aria-hidden>
|
<div className="rounded bg-white text-black px-2 py-1 text-[11px] sm:text-xs font-bold" aria-hidden>
|
||||||
ZARA
|
SIMAYA
|
||||||
</div>
|
</div>
|
||||||
<div className="font-semibold text-sm sm:text-base">{merchantName}</div>
|
<div className="font-semibold text-sm sm:text-base">{merchantName}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,17 +57,19 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
className={`text-white/80 transition-transform ${expanded ? '' : 'rotate-180'} focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-white/80 focus-visible:ring-offset-2`}
|
className={`text-white/80 transition-transform ${expanded ? '' : 'rotate-180'} focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-white/80 focus-visible:ring-offset-2`}
|
||||||
>
|
>
|
||||||
˅
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||||
|
<path d="M7 14L12 9L17 14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Amount panel */}
|
{/* Amount panel */}
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="p-4 border-b border-black/10 dark:border-white/10 flex items-start justify-between">
|
<div className="p-4 border-b border-black/10 flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Total</div>
|
<div className="text-xs text-black/60">Total</div>
|
||||||
<div className="text-xl font-semibold">{formatCurrencyIDR(amount)}</div>
|
<div className="text-xl font-semibold">{formatCurrencyIDR(amount)}</div>
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Order ID #{orderId}</div>
|
<div className="text-xs text-black/60">Order ID #{orderId}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -78,11 +80,11 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
|
||||||
</div>
|
</div>
|
||||||
{/* Sticky CTA (mobile-friendly) */}
|
{/* Sticky CTA (mobile-friendly) */}
|
||||||
{showStatusCTA && (
|
{showStatusCTA && (
|
||||||
<div className="sticky bottom-0 bg-white/95 dark:bg-neutral-900/95 backdrop-blur border-t border-black/10 dark:border-white/10 p-3 pb-[env(safe-area-inset-bottom)]">
|
<div className="sticky bottom-0 bg-white/95 backdrop-blur border-t border-black/10 p-3 pb-[env(safe-area-inset-bottom)]">
|
||||||
<Link
|
<Link
|
||||||
to={`/payments/${orderId}/status`}
|
to={`/payments/${orderId}/status`}
|
||||||
aria-label="Buka halaman status pembayaran"
|
aria-label="Buka halaman Status Pembayaran"
|
||||||
className="w-full block text-center rounded bg-[#0c1f3f] text-white py-3 text-base font-semibold hover:bg-[#0a1a35] focus:outline-none focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-[#0c1f3f]"
|
className="w-full block text-center rounded bg-[#0c1f3f] !text-white py-3 text-base font-semibold hover:bg-[#0a1a35] hover:!text-white focus:outline-none focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-[#0c1f3f] visited:!text-white active:!text-white"
|
||||||
>
|
>
|
||||||
Cek status pembayaran
|
Cek status pembayaran
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
export function TrustStrip({ location = 'panel' }: { location?: 'panel' | 'sheet' }) {
|
export function TrustStrip({ location = 'panel' }: { location?: 'panel' | 'sheet' }) {
|
||||||
return (
|
return (
|
||||||
<div className={`text-[10px] ${location === 'sheet' ? 'mt-2' : ''} text-black/50 dark:text-white/50 flex items-center gap-2`}>
|
<div className={`text-[10px] ${location === 'sheet' ? 'mt-2' : ''} text-black/50 flex items-center gap-2`}>
|
||||||
<span aria-hidden>🔒</span>
|
<span aria-hidden>🔒</span>
|
||||||
<span>Secure payments by Midtrans</span>
|
<span>Secure payments by Midtrans</span>
|
||||||
<span aria-hidden className="ml-auto flex items-center gap-1">
|
<span aria-hidden className="ml-auto flex items-center gap-1">
|
||||||
<span className="inline-block rounded bg-black/10 dark:bg-white/10 px-1">Visa</span>
|
<span className="inline-block rounded bg-black/10 px-1">Visa</span>
|
||||||
<span className="inline-block rounded bg-black/10 dark:bg-white/10 px-1">Mastercard</span>
|
<span className="inline-block rounded bg-black/10 px-1">Mastercard</span>
|
||||||
<span className="inline-block rounded bg-black/10 dark:bg-white/10 px-1">JCB</span>
|
<span className="inline-block rounded bg-black/10 px-1">JCB</span>
|
||||||
<span className="inline-block rounded bg-black/10 dark:bg-white/10 px-1">Amex</span>
|
<span className="inline-block rounded bg-black/10 px-1">Amex</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,13 @@ export function CheckoutPage() {
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PaymentSheet merchantName="Zara" orderId={orderId} amount={amount} expireAt={expireAt} showStatusCTA={currentStep === 3}>
|
<PaymentSheet merchantName="Simaya" orderId={orderId} amount={amount} expireAt={expireAt} showStatusCTA={currentStep === 3}>
|
||||||
{/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */}
|
{/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium">Konfirmasi data checkout</div>
|
<div className="text-sm font-medium">Konfirmasi data checkout</div>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Nama</div>
|
<div className="text-xs text-gray-600">Nama</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded border px-3 py-2"
|
className="w-full rounded border px-3 py-2"
|
||||||
|
|
@ -87,7 +87,7 @@ export function CheckoutPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Email atau HP</div>
|
<div className="text-xs text-gray-600">Email atau HP</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded border px-3 py-2"
|
className="w-full rounded border px-3 py-2"
|
||||||
|
|
@ -96,7 +96,7 @@ export function CheckoutPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Alamat</div>
|
<div className="text-xs text-gray-600">Alamat</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded border px-3 py-2"
|
className="w-full rounded border px-3 py-2"
|
||||||
|
|
@ -105,7 +105,7 @@ export function CheckoutPage() {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Catatan</div>
|
<div className="text-xs text-gray-600">Catatan</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full rounded border px-3 py-2"
|
className="w-full rounded border px-3 py-2"
|
||||||
value={form.notes}
|
value={form.notes}
|
||||||
|
|
@ -178,7 +178,7 @@ export function CheckoutPage() {
|
||||||
if (m === 'bank_transfer') {
|
if (m === 'bank_transfer') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div>
|
<div className="text-xs text-gray-600">Pilih bank untuk membuat Virtual Account</div>
|
||||||
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
||||||
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => (
|
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -189,7 +189,7 @@ export function CheckoutPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
|
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center overflow-hidden hover:bg-gray-100"
|
||||||
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
<BankLogo bank={bk} />
|
<BankLogo bank={bk} />
|
||||||
|
|
@ -197,8 +197,8 @@ export function CheckoutPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{isBusy && (
|
{isBusy && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60 inline-flex items-center gap-2">
|
<div className="text-xs text-gray-600 inline-flex items-center gap-2">
|
||||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
<span className="h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" aria-hidden />
|
||||||
Menyiapkan VA…
|
Menyiapkan VA…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -208,7 +208,7 @@ export function CheckoutPage() {
|
||||||
if (m === 'cstore') {
|
if (m === 'cstore') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko untuk membuat kode pembayaran</div>
|
<div className="text-xs text-gray-600">Pilih toko untuk membuat kode pembayaran</div>
|
||||||
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
||||||
{(['alfamart','indomaret'] as const).map((st) => (
|
{(['alfamart','indomaret'] as const).map((st) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -219,7 +219,7 @@ export function CheckoutPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
|
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center overflow-hidden hover:bg-gray-100"
|
||||||
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
||||||
|
|
@ -259,7 +259,7 @@ export function CheckoutPage() {
|
||||||
)}
|
)}
|
||||||
</PaymentSheet>
|
</PaymentSheet>
|
||||||
|
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">
|
<div className="text-xs text-gray-600">
|
||||||
API Base: {apiBase ?? '—'} | Client Key: {clientKey ? 'OK' : '—'}
|
API Base: {apiBase ?? '—'} | Client Key: {clientKey ? 'OK' : '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ export function DemoStorePage() {
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">Produk: T-Shirt Hitam</div>
|
<div className="text-sm font-medium">Produk: T-Shirt Hitam</div>
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Ukuran: M • Bahan: Cotton</div>
|
<div className="text-xs text-black/60">Ukuran: M • Bahan: Cotton</div>
|
||||||
<div className="mt-2 text-lg font-semibold">Rp 3.500.000</div>
|
<div className="mt-2 text-lg font-semibold">Rp 3.500.000</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-24 h-24 rounded bg-black/10 dark:bg-white/10" aria-hidden />
|
<div className="w-24 h-24 rounded bg-black/10" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export function NotFoundPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h1 className="text-xl font-semibold">Halaman tidak ditemukan</h1>
|
<h1 className="text-xl font-semibold">Halaman tidak ditemukan</h1>
|
||||||
<p className="text-sm text-black/70 dark:text-white/70">Periksa URL atau kembali ke checkout.</p>
|
<p className="text-sm text-black/70">Periksa URL atau kembali ke checkout.</p>
|
||||||
<Link to="/checkout" className="text-brand-600 underline">Kembali ke Checkout</Link>
|
<Link to="/checkout" className="text-brand-600 underline">Kembali ke Checkout</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function PayPage() {
|
||||||
}
|
}
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
const merchantName = useMemo(() => 'Demo Merchant', [])
|
const merchantName = useMemo(() => 'Simaya Retail', [])
|
||||||
|
|
||||||
const isExpired = expireAt ? Date.now() > expireAt : false
|
const isExpired = expireAt ? Date.now() > expireAt : false
|
||||||
const enabledMap: Record<PaymentMethod, boolean> = useMemo(() => {
|
const enabledMap: Record<PaymentMethod, boolean> = useMemo(() => {
|
||||||
|
|
@ -86,20 +86,20 @@ export function PayPage() {
|
||||||
expireAt={expireAt}
|
expireAt={expireAt}
|
||||||
showStatusCTA={false}
|
showStatusCTA={false}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 px-4 py-6">
|
||||||
<Alert title={title}>{msg}</Alert>
|
<Alert title={title}>{msg}</Alert>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="secondary"
|
||||||
onClick={() => { try { window.location.reload() } catch {} }}
|
onClick={() => { try { window.location.reload() } catch { } }}
|
||||||
>
|
>
|
||||||
Muat ulang
|
Muat ulang
|
||||||
</Button>
|
</Button>
|
||||||
<a
|
<a
|
||||||
href="mailto:support@example.com?subject=Permintaan%20Link%20Pembayaran&body=Order%20ID:%20"
|
href="mailto:retailimaya@gmail.com?subject=Permintaan%20Link%20Pembayaran&body=Order%20ID:%20"
|
||||||
className="inline-flex items-center px-3 py-2 rounded border bg-black text-white dark:bg-white dark:text-black"
|
className="inline-flex items-center px-3 py-2 rounded border bg-gray-800 !text-white hover:!text-white focus:!text-white visited:!text-white active:!text-white"
|
||||||
>
|
>
|
||||||
Hubungi admin
|
Hubungi Admin
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,7 +115,7 @@ export function PayPage() {
|
||||||
expireAt={expireAt}
|
expireAt={expireAt}
|
||||||
showStatusCTA={currentStep === 3}
|
showStatusCTA={currentStep === 3}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 px-4 py-6">
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PaymentMethodList
|
<PaymentMethodList
|
||||||
|
|
@ -127,7 +127,7 @@ export function PayPage() {
|
||||||
} else if (m === 'cpay') {
|
} else if (m === 'cpay') {
|
||||||
try {
|
try {
|
||||||
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
||||||
} catch {}
|
} catch { }
|
||||||
} else {
|
} else {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
|
|
@ -147,9 +147,9 @@ export function PayPage() {
|
||||||
if (m === 'bank_transfer') {
|
if (m === 'bank_transfer') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div>
|
<div className="text-xs text-gray-600">Pilih bank untuk membuat Virtual Account</div>
|
||||||
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
||||||
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => (
|
{(['bca', 'bni', 'bri', 'cimb', 'mandiri', 'permata'] as BankKey[]).map((bk) => (
|
||||||
<button
|
<button
|
||||||
key={bk}
|
key={bk}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -158,7 +158,7 @@ export function PayPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
|
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center overflow-hidden hover:bg-gray-100"
|
||||||
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
<BankLogo bank={bk} />
|
<BankLogo bank={bk} />
|
||||||
|
|
@ -166,8 +166,8 @@ export function PayPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{isBusy && (
|
{isBusy && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60 inline-flex items-center gap-2">
|
<div className="text-xs text-gray-600 inline-flex items-center gap-2">
|
||||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
<span className="h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" aria-hidden />
|
||||||
Menyiapkan VA…
|
Menyiapkan VA…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -177,9 +177,9 @@ export function PayPage() {
|
||||||
if (m === 'cstore') {
|
if (m === 'cstore') {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko untuk membuat kode pembayaran</div>
|
<div className="text-xs text-gray-600">Pilih toko untuk membuat kode pembayaran</div>
|
||||||
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
||||||
{(['alfamart','indomaret'] as const).map((st) => (
|
{(['alfamart', 'indomaret'] as const).map((st) => (
|
||||||
<button
|
<button
|
||||||
key={st}
|
key={st}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -188,7 +188,7 @@ export function PayPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
|
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center hover:bg-gray-100"
|
||||||
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,18 @@ export function PaymentStatusPage() {
|
||||||
function statusBadgeClass(s: PaymentStatusResponse['status']) {
|
function statusBadgeClass(s: PaymentStatusResponse['status']) {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return 'inline-block rounded px-2 py-0.5 text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-200'
|
return 'inline-block rounded px-2 py-0.5 text-xs bg-yellow-100 text-yellow-800'
|
||||||
case 'settlement':
|
case 'settlement':
|
||||||
case 'capture':
|
case 'capture':
|
||||||
return 'inline-block rounded px-2 py-0.5 text-xs bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200'
|
return 'inline-block rounded px-2 py-0.5 text-xs bg-green-100 text-green-800'
|
||||||
case 'deny':
|
case 'deny':
|
||||||
case 'cancel':
|
case 'cancel':
|
||||||
case 'expire':
|
case 'expire':
|
||||||
case 'refund':
|
case 'refund':
|
||||||
case 'chargeback':
|
case 'chargeback':
|
||||||
return 'inline-block rounded px-2 py-0.5 text-xs bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200'
|
return 'inline-block rounded px-2 py-0.5 text-xs bg-red-100 text-red-800'
|
||||||
default:
|
default:
|
||||||
return 'inline-block rounded px-2 py-0.5 text-xs bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-200'
|
return 'inline-block rounded px-2 py-0.5 text-xs bg-gray-100 text-gray-800'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function PaymentStatusPage() {
|
||||||
<div className="card p-4">
|
<div className="card p-4">
|
||||||
<div className="text-sm">Order ID: {orderId}</div>
|
<div className="text-sm">Order ID: {orderId}</div>
|
||||||
{method || data?.method ? (
|
{method || data?.method ? (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode: {data?.method ?? method}</div>
|
<div className="text-xs text-gray-600">Metode: {data?.method ?? method}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-2">Status: {isLoading ? (
|
<div className="mt-2">Status: {isLoading ? (
|
||||||
<span className="font-medium">memuat…</span>
|
<span className="font-medium">memuat…</span>
|
||||||
|
|
@ -62,7 +62,7 @@ export function PaymentStatusPage() {
|
||||||
) : (
|
) : (
|
||||||
<span className={statusBadgeClass(statusText)}>{statusText}</span>
|
<span className={statusBadgeClass(statusText)}>{statusText}</span>
|
||||||
)}</div>
|
)}</div>
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">
|
<div className="mt-1 text-xs text-gray-600">
|
||||||
{isFinal ? 'Status final — polling dihentikan.' : 'Polling setiap 3 detik hingga status final.'}
|
{isFinal ? 'Status final — polling dihentikan.' : 'Polling setiap 3 detik hingga status final.'}
|
||||||
</div>
|
</div>
|
||||||
{/* Method-specific details */}
|
{/* Method-specific details */}
|
||||||
|
|
@ -70,18 +70,18 @@ export function PaymentStatusPage() {
|
||||||
<div className="mt-3 space-y-2 text-sm">
|
<div className="mt-3 space-y-2 text-sm">
|
||||||
{/* Bank Transfer / VA */}
|
{/* Bank Transfer / VA */}
|
||||||
{data.vaNumber ? (
|
{data.vaNumber ? (
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-2">
|
<div className="rounded border border-gray-200 p-2">
|
||||||
<div className="font-medium">Virtual Account</div>
|
<div className="font-medium">Virtual Account</div>
|
||||||
<div>VA Number: <span className="font-mono">{data.vaNumber}</span></div>
|
<div>VA Number: <span className="font-mono">{data.vaNumber}</span></div>
|
||||||
{data.bank ? <div>Bank: {data.bank.toUpperCase()}</div> : null}
|
{data.bank ? <div>Bank: {data.bank.toUpperCase()}</div> : null}
|
||||||
{data.billKey && data.billerCode ? (
|
{data.billKey && data.billerCode ? (
|
||||||
<div className="mt-1 text-xs text-black/60 dark:text-white/60">Mandiri E-Channel — Bill Key: {data.billKey}, Biller: {data.billerCode}</div>
|
<div className="mt-1 text-xs text-gray-600">Mandiri E-Channel — Bill Key: {data.billKey}, Biller: {data.billerCode}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* C-store */}
|
{/* C-store */}
|
||||||
{data.store || data.paymentCode ? (
|
{data.store || data.paymentCode ? (
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-2">
|
<div className="rounded border border-gray-200 p-2">
|
||||||
<div className="font-medium">Convenience Store</div>
|
<div className="font-medium">Convenience Store</div>
|
||||||
{data.store ? <div>Store: {data.store}</div> : null}
|
{data.store ? <div>Store: {data.store}</div> : null}
|
||||||
{data.paymentCode ? <div>Payment Code: <span className="font-mono">{data.paymentCode}</span></div> : null}
|
{data.paymentCode ? <div>Payment Code: <span className="font-mono">{data.paymentCode}</span></div> : null}
|
||||||
|
|
@ -89,9 +89,9 @@ export function PaymentStatusPage() {
|
||||||
) : null}
|
) : null}
|
||||||
{/* E-money (GoPay/QRIS) */}
|
{/* E-money (GoPay/QRIS) */}
|
||||||
{data.actions && data.actions.length > 0 ? (
|
{data.actions && data.actions.length > 0 ? (
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-2">
|
<div className="rounded border border-gray-200 p-2">
|
||||||
<div className="font-medium">QR / Deeplink</div>
|
<div className="font-medium">QR / Deeplink</div>
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Gunakan link berikut untuk membuka aplikasi pembayaran.</div>
|
<div className="text-xs text-gray-600">Gunakan link berikut untuk membuka aplikasi pembayaran.</div>
|
||||||
<div className="mt-1 flex flex-wrap gap-2">
|
<div className="mt-1 flex flex-wrap gap-2">
|
||||||
{data.actions.map((a, i) => (
|
{data.actions.map((a, i) => (
|
||||||
<a key={i} href={a.url} target="_blank" rel="noreferrer" className="underline text-brand-600">
|
<a key={i} href={a.url} target="_blank" rel="noreferrer" className="underline text-brand-600">
|
||||||
|
|
@ -103,7 +103,7 @@ export function PaymentStatusPage() {
|
||||||
) : null}
|
) : null}
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
{data.maskedCard ? (
|
{data.maskedCard ? (
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-2">
|
<div className="rounded border border-gray-200 p-2">
|
||||||
<div className="font-medium">Kartu</div>
|
<div className="font-medium">Kartu</div>
|
||||||
<div>Masked Card: <span className="font-mono">{data.maskedCard}</span></div>
|
<div>Masked Card: <span className="font-mono">{data.maskedCard}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,15 @@ body {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
.light, html[data-theme="light"] body { background-color: #ffffff; color: #000000; }
|
|
||||||
.dark body { background-color: #000000; color: #ffffff; }
|
|
||||||
a { color: #dc2626; }
|
|
||||||
a:hover { color: #b91c1c; }
|
|
||||||
|
|
||||||
.focus-ring { outline: none; box-shadow: 0 0 0 2px rgba(220,38,38,1), 0 0 0 4px var(--focus-offset, #ffffff); }
|
a { color: #0c1f3f; }
|
||||||
.dark .focus-ring { box-shadow: 0 0 0 2px rgba(220,38,38,1), 0 0 0 4px #000000; }
|
a:hover { color: #0a1a35; }
|
||||||
|
|
||||||
|
.focus-ring { outline: none; box-shadow: 0 0 0 2px rgba(37,99,235,1), 0 0 0 4px var(--focus-offset, #ffffff); }
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid rgba(0,0,0,0.1);
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
}
|
|
||||||
.dark .card {
|
|
||||||
background-color: #000000;
|
|
||||||
border-color: rgba(255,255,255,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,20 +7,20 @@ export default {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
brand: {
|
brand: {
|
||||||
50: '#fef2f2',
|
50: '#f1f5fb',
|
||||||
100: '#fee2e2',
|
100: '#e3e9f5',
|
||||||
200: '#fecaca',
|
200: '#c7d3ea',
|
||||||
300: '#fca5a5',
|
300: '#a6b9dd',
|
||||||
400: '#f87171',
|
400: '#6f8bc8',
|
||||||
500: '#ef4444',
|
500: '#3a5da7',
|
||||||
600: '#dc2626',
|
600: '#0c1f3f',
|
||||||
700: '#b91c1c',
|
700: '#0a1a35',
|
||||||
800: '#991b1b',
|
800: '#08152a',
|
||||||
900: '#7f1d1d',
|
900: '#050f20',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
focus: '0 0 0 3px rgba(220,38,38,0.45)',
|
focus: '0 0 0 3px rgba(37,99,235,0.45)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue