Compare commits

..

No commits in common. "a9bb11f080be2cc83735342c5d0752e3139c85d4" and "01179d646ee99e93dd288240d31fbcdd2f329bc4" have entirely different histories.

6 changed files with 76 additions and 158 deletions

View File

@ -44,16 +44,26 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
</div>
<div className="font-semibold text-sm sm:text-base">{merchantName}</div>
</div>
<button
aria-label={expanded ? 'Collapse' : 'Expand'}
aria-expanded={expanded}
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`}
>
<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>
<div className="flex items-center gap-2 sm:gap-3">
<div
className="text-xs sm:text-sm text-white/80"
role="timer"
aria-live="polite"
aria-label={`Kedaluwarsa dalam ${countdown}`}
>
Kedaluwarsa dalam <span className="font-semibold text-white">{countdown}</span>
</div>
<button
aria-label={expanded ? 'Collapse' : 'Expand'}
aria-expanded={expanded}
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`}
>
<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>
</div>
</div>
{expanded && (

View File

@ -68,7 +68,6 @@ export interface PaymentStatusResponse {
// Common
transactionTime?: string
grossAmount?: string
customerName?: string // From localStorage, not from Midtrans API
// Bank transfer
vaNumber?: string
bank?: string

View File

@ -72,17 +72,6 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
Logger.paymentInfo('checkout.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
console.log('Token berhasil dibuat:', token)
// Store customer name in localStorage for status page
if (customer?.name) {
try {
const customerCache = JSON.parse(localStorage.getItem('customerCache') || '{}')
customerCache[orderId] = { name: customer.name, timestamp: Date.now() }
localStorage.setItem('customerCache', JSON.stringify(customerCache))
} catch (e) {
console.warn('Failed to cache customer name:', e)
}
}
// Verify Snap.js is loaded
console.log('window.snap:', window.snap)
console.log('window.snap.pay:', window.snap?.pay)
@ -116,7 +105,6 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
},
onClose: () => {
Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId })
console.log('🔵 Snap modal closed - calling onModalClosed')
setLoading(false)
onModalClosed?.() // Enable status button when modal closed
}
@ -140,7 +128,6 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
}
onError?.(e)
onModalClosed?.() // Enable status button on error
setLoading(false)
}
}
@ -328,13 +315,6 @@ export function CheckoutPage() {
{(() => {
console.log('Rendering step 2 - AutoSnapPayment', { orderId, amount, currentStep })
Logger.info('checkout.step2.render', { orderId, amount })
// Fallback: Force show status button after 3 seconds if callback not called
setTimeout(() => {
if (!modalClosed) {
console.log('⚠️ Fallback timer: Forcing status button to show')
setModalClosed(true)
}
}, 3000)
return null
})()}
<AutoSnapPayment
@ -355,7 +335,6 @@ export function CheckoutPage() {
// Handle payment error
}}
onModalClosed={() => {
console.log('🟢 onModalClosed callback fired - setting modalClosed to TRUE')
setModalClosed(true) // Enable status button when modal closed
}}
/>

View File

@ -73,17 +73,6 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Auto
Logger.paymentInfo('paypage.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
console.log('[PayPage] Token received:', token)
// Store customer name in localStorage for status page
if (customer?.name) {
try {
const customerCache = JSON.parse(localStorage.getItem('customerCache') || '{}')
customerCache[orderId] = { name: customer.name, timestamp: Date.now() }
localStorage.setItem('customerCache', JSON.stringify(customerCache))
} catch (e) {
console.warn('Failed to cache customer name:', e)
}
}
if (!window.snap || typeof window.snap.pay !== 'function') {
throw new Error(`Snap.js not loaded: hasSnap=${!!window.snap}`)
}

View File

@ -1,11 +1,14 @@
import React from 'react'
import { useParams, useSearchParams } from 'react-router-dom'
import { usePaymentNavigation } from '../features/payments/lib/navigation'
import { usePaymentStatus } from '../features/payments/lib/usePaymentStatus'
import type { PaymentStatusResponse } from '../features/payments/lib/midtrans'
import { Logger } from '../lib/logger'
import { CountdownRedirect } from '../components/CountdownRedirect'
export function PaymentStatusPage() {
const { orderId } = useParams()
const nav = usePaymentNavigation()
const [search] = useSearchParams()
const method = (search.get('m') ?? undefined) as ('bank_transfer' | 'gopay' | 'qris' | 'cstore' | 'credit_card' | undefined)
const { data, isLoading, error } = usePaymentStatus(orderId)
@ -60,15 +63,9 @@ export function PaymentStatusPage() {
const [qrSrc, setQrSrc] = React.useState<string>('')
React.useEffect(() => { setQrSrc(qrCandidates[0] || '') }, [statusText, method, orderId, data, qrCandidates])
// Get customer name from localStorage
const customerName = React.useMemo(() => {
try {
const customerCache = JSON.parse(localStorage.getItem('customerCache') || '{}')
return customerCache[orderId || '']?.name
} catch {
return undefined
}
}, [orderId])
function handleRedirect() {
nav.toHistory()
}
// Logs for debugging status lifecycle
React.useEffect(() => {
@ -158,13 +155,6 @@ export function PaymentStatusPage() {
)}
</div>
{customerName ? (
<div className="mb-4">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Nama Pelanggan</div>
<div className="text-sm font-medium text-gray-900">{customerName}</div>
</div>
) : null}
{method || data?.method ? (
<div className="mb-4">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Metode Pembayaran</div>
@ -174,6 +164,18 @@ export function PaymentStatusPage() {
</div>
</div>
{isSuccess ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div className="flex items-start gap-3">
<div className="text-2xl">🎉</div>
<div className="flex-1">
<div className="font-semibold text-green-900 mb-1">Transaksi Selesai!</div>
<div className="text-sm text-green-700">Anda akan diarahkan ke halaman utama dalam beberapa detik</div>
<CountdownRedirect seconds={5} destination="dashboard" onComplete={handleRedirect} />
</div>
</div>
</div>
) : null}
{/* Payment Instructions - Only show for pending status */}
{!isLoading && !error && data && statusText === 'pending' ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden mb-4">
@ -181,85 +183,43 @@ export function PaymentStatusPage() {
<div className="text-sm font-semibold text-blue-900">📝 Cara Pembayaran</div>
</div>
<div className="p-6 space-y-4">
{/* Bank Transfer OR Mandiri E-Channel */}
{(!method || method === 'bank_transfer' || data.method === 'bank_transfer' || data.method === 'echannel') && (data.vaNumber || (data.billKey && data.billerCode)) ? (
{(!method || method === 'bank_transfer') && data.vaNumber ? (
<>
{data.vaNumber ? (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-2">Nomor Virtual Account</div>
<div className="flex items-center justify-between bg-white rounded border border-gray-300 px-4 py-3">
<div className="font-mono text-lg font-bold text-gray-900">{data.vaNumber}</div>
<button
onClick={() => navigator.clipboard.writeText(data.vaNumber || '')}
className="text-xs bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700"
>
📋 Salin
</button>
</div>
{data.bank ? (
<div className="mt-2">
<div className="text-xs text-gray-500">Bank</div>
<div className="text-sm font-semibold text-gray-900 uppercase">{data.bank}</div>
</div>
) : null}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-2">Nomor Virtual Account</div>
<div className="flex items-center justify-between bg-white rounded border border-gray-300 px-4 py-3">
<div className="font-mono text-lg font-bold text-gray-900">{data.vaNumber}</div>
<button
onClick={() => navigator.clipboard.writeText(data.vaNumber || '')}
className="text-xs bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700"
>
📋 Salin
</button>
</div>
) : null}
{/* Mandiri E-Channel specific */}
{data.billKey && data.billerCode ? (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="font-semibold text-yellow-900 mb-3 text-base">💳 Mandiri E-Channel</div>
<div className="space-y-3">
<div>
<div className="text-xs text-yellow-700 uppercase tracking-wide mb-1">Kode Perusahaan (Biller Code)</div>
<div className="flex items-center justify-between bg-white rounded border border-yellow-300 px-4 py-3">
<div className="font-mono text-lg font-bold text-gray-900">{data.billerCode}</div>
<button
onClick={() => navigator.clipboard.writeText(data.billerCode || '')}
className="text-xs bg-yellow-600 text-white px-3 py-1.5 rounded hover:bg-yellow-700"
>
📋 Salin
</button>
</div>
</div>
<div>
<div className="text-xs text-yellow-700 uppercase tracking-wide mb-1">Kode Bayar (Bill Key)</div>
<div className="flex items-center justify-between bg-white rounded border border-yellow-300 px-4 py-3">
<div className="font-mono text-lg font-bold text-gray-900">{data.billKey}</div>
<button
onClick={() => navigator.clipboard.writeText(data.billKey || '')}
className="text-xs bg-yellow-600 text-white px-3 py-1.5 rounded hover:bg-yellow-700"
>
📋 Salin
</button>
</div>
</div>
{data.bank ? (
<div className="mt-2">
<div className="text-xs text-gray-500">Bank</div>
<div className="text-sm font-semibold text-gray-900 uppercase">{data.bank}</div>
</div>
</div>
) : null}
) : null}
</div>
<div className="text-sm text-gray-600 space-y-2">
<p className="font-medium text-gray-900">Langkah pembayaran:</p>
{data.billKey && data.billerCode ? (
<ol className="list-decimal list-inside space-y-1 ml-2">
<li>Buka aplikasi Livin' by Mandiri atau ATM Mandiri</li>
<li>Pilih menu <strong>Bayar</strong> / <strong>Multi Payment</strong></li>
<li>Pilih penyedia jasa: <strong>Midtrans</strong> (atau cari dengan Biller Code)</li>
<li>Masukkan Kode Perusahaan: <strong>{data.billerCode}</strong></li>
<li>Masukkan Kode Bayar: <strong>{data.billKey}</strong></li>
<li>Periksa detail tagihan dan konfirmasi pembayaran</li>
<li>Simpan bukti transaksi</li>
</ol>
) : (
<ol className="list-decimal list-inside space-y-1 ml-2">
<li>Buka aplikasi mobile banking atau ATM</li>
<li>Pilih menu Transfer / Bayar</li>
<li>Masukkan nomor Virtual Account di atas</li>
<li>Konfirmasi pembayaran</li>
<li>Simpan bukti transaksi</li>
</ol>
)}
<ol className="list-decimal list-inside space-y-1 ml-2">
<li>Buka aplikasi mobile banking atau ATM</li>
<li>Pilih menu Transfer / Bayar</li>
<li>Masukkan nomor Virtual Account di atas</li>
<li>Konfirmasi pembayaran</li>
<li>Simpan bukti transaksi</li>
</ol>
</div>
{data.billKey && data.billerCode ? (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm">
<div className="font-medium text-yellow-900 mb-1">Khusus Mandiri E-Channel:</div>
<div className="text-yellow-800">Kode Biller: <span className="font-mono font-semibold">{data.billerCode}</span></div>
<div className="text-yellow-800">Kode Bayar: <span className="font-mono font-semibold">{data.billKey}</span></div>
</div>
) : null}
</>
) : null}
{(!method || method === 'cstore') && (data.store || data.paymentCode) ? (
@ -297,7 +257,7 @@ export function PaymentStatusPage() {
</div>
</>
) : null}
{(!method || method === 'gopay' || method === 'qris' || data.method === 'qris' || data.method === 'gopay') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? (
{(!method || method === 'gopay' || method === 'qris') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? (
<>
{qrSrc ? (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
@ -313,7 +273,7 @@ export function PaymentStatusPage() {
<div className="text-sm text-gray-600 space-y-2">
<p className="font-medium text-gray-900">Langkah pembayaran:</p>
<ol className="list-decimal list-inside space-y-1 ml-2">
<li>Buka aplikasi {method === 'gopay' || data.method === 'gopay' ? 'GoPay/Gojek' : 'e-wallet atau m-banking yang mendukung QRIS'}</li>
<li>Buka aplikasi {method === 'gopay' ? 'GoPay/Gojek' : 'e-wallet atau m-banking'}</li>
<li>Pilih menu Scan QR atau QRIS</li>
<li>Arahkan kamera ke QR code di atas</li>
<li>Konfirmasi pembayaran</li>
@ -335,25 +295,6 @@ export function PaymentStatusPage() {
</div>
) : null}
</>
) : (data.method === 'qris' || data.method === 'gopay') ? (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-2xl">📱</div>
<div className="flex-1">
<div className="font-semibold text-blue-900 mb-2">QR Code Pembayaran</div>
<div className="text-sm text-blue-700 space-y-2">
<p>QR code untuk pembayaran ini ditampilkan di jendela pembayaran Snap.</p>
<p>Jika Anda menutup jendela tersebut, silakan:</p>
<ol className="list-decimal list-inside ml-2 mt-2 space-y-1">
<li>Kembali ke halaman checkout</li>
<li>Buat pembayaran baru dengan order ID yang sama</li>
<li>QR code akan muncul kembali di jendela Snap</li>
</ol>
<p className="mt-3 text-blue-600 italic">Atau tunggu hingga pembayaran kedaluwarsa dan buat transaksi baru.</p>
</div>
</div>
</div>
</div>
) : null}
{(!method || method === 'credit_card') && data.maskedCard ? (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">

View File

@ -1,12 +1,12 @@
{
"mercant_id": "REFNO-004",
"mercant_id": "REFNO-003",
"timestamp": 1733331600000,
"deskripsi": "Bayar Internet",
"nominal": 250000,
"nama": "Test User 4",
"no_telepon": "081234567892",
"email": "test4@example.com",
"nama": "Test User 3",
"no_telepon": "081234567891",
"email": "test3@example.com",
"item": [
{ "item_id": "TKG-2512043", "nama": "Internet Desember Premium", "harga": 250000, "qty": 1 }
{ "item_id": "TKG-2512042", "nama": "Internet Desember Premium", "harga": 250000, "qty": 1 }
]
}