diff --git a/public/logos/logo-semua-bank.PNG b/public/logos/logo-semua-bank.PNG new file mode 100644 index 0000000..093f718 Binary files /dev/null and b/public/logos/logo-semua-bank.PNG differ diff --git a/src/features/payments/components/BankTransferPanel.tsx b/src/features/payments/components/BankTransferPanel.tsx index 457a5eb..61cc295 100644 --- a/src/features/payments/components/BankTransferPanel.tsx +++ b/src/features/payments/components/BankTransferPanel.tsx @@ -4,9 +4,10 @@ import React from 'react' import { PaymentInstructions } from './PaymentInstructions' import { BcaInstructionList } from './BcaInstructionList' import { TrustStrip } from './TrustStrip' -import { BankLogosRow, BankLogo, type BankKey } from './PaymentLogos' +import { type BankKey } from './PaymentLogos' import { postCharge } from '../../../services/api' import { Alert } from '../../../components/alert/Alert' +import { InlinePaymentStatus } from './InlinePaymentStatus' // Global guard to prevent duplicate auto-charge across StrictMode double-mounts const attemptedChargeKeys = new Set() @@ -137,11 +138,9 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, return (
Transfer bank
- {selected && (
Bank: - {selected.toUpperCase()}
)} @@ -155,7 +154,10 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
Virtual Account
{vaCode ? ( - Nomor VA: {vaCode} + + Nomor VA: + {vaCode} + ) : ( {busy && } @@ -176,6 +178,10 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
)} + {/* Status inline dengan polling otomatis */} + {selected && ( + + )} {selected && (
{selected === 'bca' ? ( @@ -281,12 +287,12 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, )}
diff --git a/src/features/payments/components/CStorePanel.tsx b/src/features/payments/components/CStorePanel.tsx index ae8cb49..8c524c2 100644 --- a/src/features/payments/components/CStorePanel.tsx +++ b/src/features/payments/components/CStorePanel.tsx @@ -4,6 +4,7 @@ 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' @@ -115,13 +116,14 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
+ diff --git a/src/features/payments/components/CardPanel.tsx b/src/features/payments/components/CardPanel.tsx index d0286e9..a7940c6 100644 --- a/src/features/payments/components/CardPanel.tsx +++ b/src/features/payments/components/CardPanel.tsx @@ -8,6 +8,7 @@ import { ensureMidtrans3ds, getCardToken, authenticate3ds } from '../lib/midtran import { Logger } from '../../../lib/logger' import { Env } from '../../../lib/env' import { postCharge } from '../../../services/api' +import { InlinePaymentStatus } from './InlinePaymentStatus' export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) { const nav = usePaymentNavigation() @@ -145,14 +146,16 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde ) : 'Bayar sekarang'} + {/* Status inline dengan polling otomatis */} + ) diff --git a/src/features/payments/components/GoPayPanel.tsx b/src/features/payments/components/GoPayPanel.tsx index d7856d6..bdc6b6e 100644 --- a/src/features/payments/components/GoPayPanel.tsx +++ b/src/features/payments/components/GoPayPanel.tsx @@ -5,6 +5,7 @@ import { PaymentInstructions } from './PaymentInstructions' import { TrustStrip } from './TrustStrip' import { GoPayLogosRow } from './PaymentLogos' import { postCharge } from '../../../services/api' +import { InlinePaymentStatus } from './InlinePaymentStatus' // Global guards/tasks to stabilize QR generation across StrictMode remounts const attemptedChargeKeys = new Set() @@ -135,20 +136,23 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.
)}
- + +
+ +
diff --git a/src/features/payments/components/InlinePaymentStatus.tsx b/src/features/payments/components/InlinePaymentStatus.tsx new file mode 100644 index 0000000..d70ff4c --- /dev/null +++ b/src/features/payments/components/InlinePaymentStatus.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { Button } from '../../../components/ui/button' +import { usePaymentNavigation } from '../lib/navigation' +import { usePaymentStatus } from '../lib/usePaymentStatus' +import type { PaymentStatusResponse } from '../lib/midtrans' + +function formatIDR(amount?: string) { + if (!amount) return '' + const n = Number(amount) + if (Number.isNaN(n)) return amount + return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(Math.round(n)) +} + +export function InlinePaymentStatus({ orderId, method, compact }: { orderId: string; method?: string; compact?: boolean }) { + const nav = usePaymentNavigation() + const { data, isLoading, error, refetch, isRefetching } = usePaymentStatus(orderId) + const status = (data?.status ?? 'pending') as PaymentStatusResponse['status'] + const isFinal = ['settlement', 'capture', 'expire', 'cancel', 'deny', 'refund', 'chargeback'].includes(status) + const isSuccess = status === 'settlement' || status === 'capture' + const isFailure = ['deny', 'cancel', 'expire', 'refund', 'chargeback'].includes(status) + + return ( +
+ {/* Header minimal tanpa detail teknis */} +
Status pembayaran
+ + {/* Konten berdasarkan status */} + {isLoading ? ( +
+ + + Mengecek pembayaran… + +
Kami memeriksa otomatis setiap 3 detik.
+
+ ) : error ? ( +
Gagal memuat status. Coba refresh.
+ ) : isSuccess ? ( +
+
+ + {/* check icon */} + + + + +
Pembayaran berhasil
+
+ {data?.grossAmount ? ( +
Total dibayar: {formatIDR(data.grossAmount)}
+ ) : null} +
Terima kasih! Pesanan Anda sedang diproses.
+
+ + + +
+
+ ) : isFailure ? ( +
+
+ + {/* x icon */} + + + + + +
Pembayaran belum berhasil
+
+
Silakan coba lagi atau pilih metode lain.
+
+ + +
+
+ ) : ( +
+
+ + {/* hourglass/spinner icon */} + + +
Menunggu pembayaran
+
+
Kami memeriksa otomatis setiap 3 detik sampai selesai.
+
+ + +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/features/payments/components/PaymentLogos.tsx b/src/features/payments/components/PaymentLogos.tsx index c0482d1..f3a5c79 100644 --- a/src/features/payments/components/PaymentLogos.tsx +++ b/src/features/payments/components/PaymentLogos.tsx @@ -6,13 +6,13 @@ function toProxy(url: string) { return `https://images.weserv.nl/?url=${encodeURIComponent(url)}` } -function BrandImg({ src, alt, compact = false, fallbackSrc }: { src: string; alt: string; compact?: boolean; fallbackSrc?: string }) { - const sizeClass = compact ? 'h-5' : 'h-6' +function BrandImg({ src, alt, compact = false, size, fallbackSrc }: { src: string; alt: string; compact?: boolean; size?: 'xs' | 'sm' | 'md'; fallbackSrc?: string }) { + const sizeClass = size ? (size === 'xs' ? 'h-4' : size === 'sm' ? 'h-5' : 'h-6') : (compact ? 'h-5' : 'h-6') return ( {alt} { @@ -45,23 +45,23 @@ const BANK_LOGOS: Record = { permata: { alt: 'PermataBank', src: '/logos/permata Bank.png', fb: 'https://upload.wikimedia.org/wikipedia/commons/4/42/PermataBank_logo.svg' }, } -export function BankLogo({ bank, compact = false }: { bank: BankKey; compact?: boolean }) { +export function BankLogo({ bank, compact = false, size }: { bank: BankKey; compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { const cfg = BANK_LOGOS[bank] - return + return } -export function BankLogosRow({ compact = false }: { compact?: boolean }) { +export function BankLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { const all: BankKey[] = ['bca', 'bni', 'bri', 'cimb', 'mandiri', 'permata'] return (
{all.map((k) => ( - + ))}
) } -export function CardLogosRow({ compact = false }: { compact?: boolean }) { +export function CardLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { const logos = [ { alt: 'Visa', src: '/logos/visa.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/0/04/Visa.svg' }, { alt: 'Mastercard', src: '/logos/mastercard.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg' }, @@ -71,25 +71,48 @@ export function CardLogosRow({ compact = false }: { compact?: boolean }) { return (
{logos.map((l) => ( - + ))}
) } -export function LogoGoPay({ compact = false }: { compact?: boolean }) { - return +export function LogoGoPay({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + // Gunakan file lokal yang tersedia di public/logos/ + return } -export function LogoQRIS({ compact = false }: { compact?: boolean }) { - return +export function LogoQRIS({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + // Gunakan file lokal yang tersedia di public/logos/ + return } -export function GoPayLogosRow({ compact = false }: { compact?: boolean }) { +export function LogoAlfamart({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + return +} + +export function LogoIndomaret({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + return +} + +export function LogoCpay({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + // Sumber lokal sesuai permintaan: public/logos/Cifo_cpay.png + return +} + +export function CStoreLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { return (
- - + + +
+ ) +} +export function GoPayLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) { + return ( +
+ +
) } \ No newline at end of file diff --git a/src/features/payments/components/PaymentMethodList.tsx b/src/features/payments/components/PaymentMethodList.tsx index 2f293aa..e62af20 100644 --- a/src/features/payments/components/PaymentMethodList.tsx +++ b/src/features/payments/components/PaymentMethodList.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { BankLogosRow, CardLogosRow, GoPayLogosRow } from './PaymentLogos' +import { CardLogosRow, GoPayLogosRow, CStoreLogosRow, LogoCpay } from './PaymentLogos' import { Env } from '../../../lib/env' -export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore' +export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore' | 'cpay' export interface PaymentMethodListProps { selected?: PaymentMethod @@ -13,18 +13,20 @@ export interface PaymentMethodListProps { } 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: }, - { key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: }, - { key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: }, - { key: 'cstore', title: 'Convenience Store', subtitle: 'Alfamart • Indomaret' }, + { key: 'bank_transfer', title: 'Transfer bank', subtitle: 'BCA • BNI • BRI • CIMB • Mandiri • Permata', icon: Semua bank yang didukung }, + { key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: }, + { key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: }, + { key: 'cstore', title: 'Convenience Store', subtitle: '', icon: }, + { key: 'cpay', title: 'cPay', subtitle: 'Bayar via aplikasi CIFO Token', icon: }, ] export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, enabled }: PaymentMethodListProps) { - const enabledMap: Record = enabled ?? { - bank_transfer: Env.ENABLE_BANK_TRANSFER, - credit_card: Env.ENABLE_CREDIT_CARD, - gopay: Env.ENABLE_GOPAY, - cstore: Env.ENABLE_CSTORE, + const enabledMap: Record = { + bank_transfer: enabled?.bank_transfer ?? Env.ENABLE_BANK_TRANSFER, + credit_card: enabled?.credit_card ?? Env.ENABLE_CREDIT_CARD, + gopay: enabled?.gopay ?? Env.ENABLE_GOPAY, + cstore: enabled?.cstore ?? Env.ENABLE_CSTORE, + cpay: enabled?.cpay ?? Env.ENABLE_CPAY, } const items = baseItems.filter((it) => enabledMap[it.key]) return ( @@ -41,14 +43,25 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e aria-expanded={selected === it.key} aria-controls={`panel-${it.key}`} > -
+
{it.title}
-
{it.subtitle}
+ {it.key === 'bank_transfer' && it.subtitle && ( +
+ {it.subtitle} +
+ )} + {it.key === 'cpay' && it.subtitle && ( +
+ {it.subtitle} +
+ )}
- - {it.icon} - + {it.icon && ( + + {it.icon} + + )}
diff --git a/src/features/payments/components/PaymentSheet.tsx b/src/features/payments/components/PaymentSheet.tsx index fbcbb09..2f8d2fe 100644 --- a/src/features/payments/components/PaymentSheet.tsx +++ b/src/features/payments/components/PaymentSheet.tsx @@ -25,9 +25,10 @@ export interface PaymentSheetProps { amount: number expireAt: number // epoch ms children?: React.ReactNode + showStatusCTA?: boolean } -export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children }: PaymentSheetProps) { +export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) { const countdown = useCountdown(expireAt) const [expanded, setExpanded] = React.useState(true) return ( @@ -76,15 +77,17 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
{/* Sticky CTA (mobile-friendly) */} -
- - Cek status pembayaran - -
+ {showStatusCTA && ( +
+ + Cek status pembayaran + +
+ )} ) diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index 2c91718..00bf98b 100644 --- a/src/pages/CheckoutPage.tsx +++ b/src/pages/CheckoutPage.tsx @@ -8,7 +8,7 @@ import { BankTransferPanel } from '../features/payments/components/BankTransferP import { CardPanel } from '../features/payments/components/CardPanel' import { GoPayPanel } from '../features/payments/components/GoPayPanel' import { CStorePanel } from '../features/payments/components/CStorePanel' -import { BankLogo, type BankKey } from '../features/payments/components/PaymentLogos' +import { BankLogo, type BankKey, LogoAlfamart, LogoIndomaret } from '../features/payments/components/PaymentLogos' import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig' import { Logger } from '../lib/logger' import React from 'react' @@ -72,7 +72,7 @@ export function CheckoutPage() { )} - + {/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */} {currentStep === 1 && (
@@ -142,6 +142,15 @@ export function CheckoutPage() { setSelected(m) if (m === 'bank_transfer' || m === 'cstore') { // Panel akan tampil di bawah item menggunakan renderPanel + } else if (m === 'cpay') { + // Redirect ke aplikasi cPay (CIFO Token) di Play Store + try { + Logger.info('cpay.redirect.start') + window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank') + Logger.info('cpay.redirect.done') + } catch (e) { + Logger.error('cpay.redirect.error', { message: (e as Error)?.message }) + } } else { setIsBusy(true) setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300) @@ -161,6 +170,13 @@ export function CheckoutPage() { if (m === 'bank_transfer') { return (
+
+ Logo semua bank yang didukung +
Pilih bank untuk membuat Virtual Account
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => ( @@ -172,11 +188,10 @@ export function CheckoutPage() { setIsBusy(true) 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 flex-col items-center gap-1 hover:bg-black/5 dark:hover:bg-white/10" + 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" aria-label={`Pilih bank ${bk.toUpperCase()}`} > - {bk.toUpperCase()} ))}
@@ -203,10 +218,10 @@ export function CheckoutPage() { setIsBusy(true) 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 flex-col items-center gap-1 hover:bg-black/5 dark:hover:bg-white/10" + 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" aria-label={`Pilih toko ${st.toUpperCase()}`} > - {st.toUpperCase()} + {st === 'alfamart' ? : } ))}
@@ -255,5 +270,6 @@ function defaultEnabled(): Record { credit_card: Env.ENABLE_CREDIT_CARD, gopay: Env.ENABLE_GOPAY, cstore: Env.ENABLE_CSTORE, + cpay: Env.ENABLE_CPAY, } } \ No newline at end of file