UX FIX #17
|
|
@ -44,15 +44,6 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
|
|||
</div>
|
||||
<div className="font-semibold text-sm sm:text-base">{merchantName}</div>
|
||||
</div>
|
||||
<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}
|
||||
|
|
@ -64,7 +55,6 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="p-4 border-b border-black/10 flex items-start justify-between">
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export interface PaymentStatusResponse {
|
|||
// Common
|
||||
transactionTime?: string
|
||||
grossAmount?: string
|
||||
customerName?: string // From localStorage, not from Midtrans API
|
||||
// Bank transfer
|
||||
vaNumber?: string
|
||||
bank?: string
|
||||
|
|
|
|||
|
|
@ -72,6 +72,17 @@ 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)
|
||||
|
|
@ -105,6 +116,7 @@ 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
|
||||
}
|
||||
|
|
@ -128,6 +140,7 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
|
|||
}
|
||||
|
||||
onError?.(e)
|
||||
onModalClosed?.() // Enable status button on error
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -315,6 +328,13 @@ 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
|
||||
|
|
@ -335,6 +355,7 @@ export function CheckoutPage() {
|
|||
// Handle payment error
|
||||
}}
|
||||
onModalClosed={() => {
|
||||
console.log('🟢 onModalClosed callback fired - setting modalClosed to TRUE')
|
||||
setModalClosed(true) // Enable status button when modal closed
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,17 @@ 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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
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)
|
||||
|
|
@ -63,9 +60,15 @@ export function PaymentStatusPage() {
|
|||
const [qrSrc, setQrSrc] = React.useState<string>('')
|
||||
React.useEffect(() => { setQrSrc(qrCandidates[0] || '') }, [statusText, method, orderId, data, qrCandidates])
|
||||
|
||||
function handleRedirect() {
|
||||
nav.toHistory()
|
||||
// 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])
|
||||
|
||||
// Logs for debugging status lifecycle
|
||||
React.useEffect(() => {
|
||||
|
|
@ -155,6 +158,13 @@ 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>
|
||||
|
|
@ -164,18 +174,6 @@ 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">
|
||||
|
|
@ -183,8 +181,10 @@ export function PaymentStatusPage() {
|
|||
<div className="text-sm font-semibold text-blue-900">📝 Cara Pembayaran</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{(!method || method === 'bank_transfer') && data.vaNumber ? (
|
||||
{/* Bank Transfer OR Mandiri E-Channel */}
|
||||
{(!method || method === 'bank_transfer' || data.method === 'bank_transfer' || data.method === 'echannel') && (data.vaNumber || (data.billKey && data.billerCode)) ? (
|
||||
<>
|
||||
{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">
|
||||
|
|
@ -203,8 +203,54 @@ export function PaymentStatusPage() {
|
|||
</div>
|
||||
) : null}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<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>
|
||||
|
|
@ -212,14 +258,8 @@ export function PaymentStatusPage() {
|
|||
<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) ? (
|
||||
|
|
@ -257,7 +297,7 @@ export function PaymentStatusPage() {
|
|||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{(!method || method === 'gopay' || method === 'qris') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? (
|
||||
{(!method || method === 'gopay' || method === 'qris' || data.method === 'qris' || data.method === 'gopay') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? (
|
||||
<>
|
||||
{qrSrc ? (
|
||||
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||
|
|
@ -273,7 +313,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' ? 'GoPay/Gojek' : 'e-wallet atau m-banking'}</li>
|
||||
<li>Buka aplikasi {method === 'gopay' || data.method === 'gopay' ? 'GoPay/Gojek' : 'e-wallet atau m-banking yang mendukung QRIS'}</li>
|
||||
<li>Pilih menu Scan QR atau QRIS</li>
|
||||
<li>Arahkan kamera ke QR code di atas</li>
|
||||
<li>Konfirmasi pembayaran</li>
|
||||
|
|
@ -295,6 +335,25 @@ 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">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"mercant_id": "REFNO-003",
|
||||
"mercant_id": "REFNO-004",
|
||||
"timestamp": 1733331600000,
|
||||
"deskripsi": "Bayar Internet",
|
||||
"nominal": 250000,
|
||||
"nama": "Test User 3",
|
||||
"no_telepon": "081234567891",
|
||||
"email": "test3@example.com",
|
||||
"nama": "Test User 4",
|
||||
"no_telepon": "081234567892",
|
||||
"email": "test4@example.com",
|
||||
"item": [
|
||||
{ "item_id": "TKG-2512042", "nama": "Internet Desember Premium", "harga": 250000, "qty": 1 }
|
||||
{ "item_id": "TKG-2512043", "nama": "Internet Desember Premium", "harga": 250000, "qty": 1 }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue