UX FIX #17

Merged
root merged 1 commits from epic-6-snap-hybrid-complete into dev 2025-12-04 10:01:04 +00:00
6 changed files with 160 additions and 78 deletions

View File

@ -44,15 +44,6 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
</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>
<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 <button
aria-label={expanded ? 'Collapse' : 'Expand'} aria-label={expanded ? 'Collapse' : 'Expand'}
aria-expanded={expanded} aria-expanded={expanded}
@ -64,7 +55,6 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
</svg> </svg>
</button> </button>
</div> </div>
</div>
{expanded && ( {expanded && (
<div className="p-4 border-b border-black/10 flex items-start justify-between"> <div className="p-4 border-b border-black/10 flex items-start justify-between">

View File

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

View File

@ -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) + '...' }) Logger.paymentInfo('checkout.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
console.log('Token berhasil dibuat:', token) 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 // Verify Snap.js is loaded
console.log('window.snap:', window.snap) console.log('window.snap:', window.snap)
console.log('window.snap.pay:', window.snap?.pay) console.log('window.snap.pay:', window.snap?.pay)
@ -105,6 +116,7 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
}, },
onClose: () => { onClose: () => {
Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId }) Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId })
console.log('🔵 Snap modal closed - calling onModalClosed')
setLoading(false) setLoading(false)
onModalClosed?.() // Enable status button when modal closed onModalClosed?.() // Enable status button when modal closed
} }
@ -128,6 +140,7 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModa
} }
onError?.(e) onError?.(e)
onModalClosed?.() // Enable status button on error
setLoading(false) setLoading(false)
} }
} }
@ -315,6 +328,13 @@ export function CheckoutPage() {
{(() => { {(() => {
console.log('Rendering step 2 - AutoSnapPayment', { orderId, amount, currentStep }) console.log('Rendering step 2 - AutoSnapPayment', { orderId, amount, currentStep })
Logger.info('checkout.step2.render', { orderId, amount }) 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 return null
})()} })()}
<AutoSnapPayment <AutoSnapPayment
@ -335,6 +355,7 @@ export function CheckoutPage() {
// Handle payment error // Handle payment error
}} }}
onModalClosed={() => { onModalClosed={() => {
console.log('🟢 onModalClosed callback fired - setting modalClosed to TRUE')
setModalClosed(true) // Enable status button when modal closed setModalClosed(true) // Enable status button when modal closed
}} }}
/> />

View File

@ -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) + '...' }) Logger.paymentInfo('paypage.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
console.log('[PayPage] Token received:', token) 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') { if (!window.snap || typeof window.snap.pay !== 'function') {
throw new Error(`Snap.js not loaded: hasSnap=${!!window.snap}`) throw new Error(`Snap.js not loaded: hasSnap=${!!window.snap}`)
} }

View File

@ -1,14 +1,11 @@
import React from 'react' import React from 'react'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { usePaymentNavigation } from '../features/payments/lib/navigation'
import { usePaymentStatus } from '../features/payments/lib/usePaymentStatus' import { usePaymentStatus } from '../features/payments/lib/usePaymentStatus'
import type { PaymentStatusResponse } from '../features/payments/lib/midtrans' import type { PaymentStatusResponse } from '../features/payments/lib/midtrans'
import { Logger } from '../lib/logger' import { Logger } from '../lib/logger'
import { CountdownRedirect } from '../components/CountdownRedirect'
export function PaymentStatusPage() { export function PaymentStatusPage() {
const { orderId } = useParams() const { orderId } = useParams()
const nav = usePaymentNavigation()
const [search] = useSearchParams() const [search] = useSearchParams()
const method = (search.get('m') ?? undefined) as ('bank_transfer' | 'gopay' | 'qris' | 'cstore' | 'credit_card' | undefined) const method = (search.get('m') ?? undefined) as ('bank_transfer' | 'gopay' | 'qris' | 'cstore' | 'credit_card' | undefined)
const { data, isLoading, error } = usePaymentStatus(orderId) const { data, isLoading, error } = usePaymentStatus(orderId)
@ -63,9 +60,15 @@ export function PaymentStatusPage() {
const [qrSrc, setQrSrc] = React.useState<string>('') const [qrSrc, setQrSrc] = React.useState<string>('')
React.useEffect(() => { setQrSrc(qrCandidates[0] || '') }, [statusText, method, orderId, data, qrCandidates]) React.useEffect(() => { setQrSrc(qrCandidates[0] || '') }, [statusText, method, orderId, data, qrCandidates])
function handleRedirect() { // Get customer name from localStorage
nav.toHistory() 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 // Logs for debugging status lifecycle
React.useEffect(() => { React.useEffect(() => {
@ -155,6 +158,13 @@ export function PaymentStatusPage() {
)} )}
</div> </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 ? ( {method || data?.method ? (
<div className="mb-4"> <div className="mb-4">
<div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Metode Pembayaran</div> <div className="text-xs text-gray-500 uppercase tracking-wide mb-1">Metode Pembayaran</div>
@ -164,18 +174,6 @@ export function PaymentStatusPage() {
</div> </div>
</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 */} {/* Payment Instructions - Only show for pending status */}
{!isLoading && !error && data && statusText === 'pending' ? ( {!isLoading && !error && data && statusText === 'pending' ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden mb-4"> <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 className="text-sm font-semibold text-blue-900">📝 Cara Pembayaran</div>
</div> </div>
<div className="p-6 space-y-4"> <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="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="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="flex items-center justify-between bg-white rounded border border-gray-300 px-4 py-3">
@ -203,8 +203,54 @@ export function PaymentStatusPage() {
</div> </div>
) : null} ) : null}
</div> </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"> <div className="text-sm text-gray-600 space-y-2">
<p className="font-medium text-gray-900">Langkah pembayaran:</p> <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"> <ol className="list-decimal list-inside space-y-1 ml-2">
<li>Buka aplikasi mobile banking atau ATM</li> <li>Buka aplikasi mobile banking atau ATM</li>
<li>Pilih menu Transfer / Bayar</li> <li>Pilih menu Transfer / Bayar</li>
@ -212,14 +258,8 @@ export function PaymentStatusPage() {
<li>Konfirmasi pembayaran</li> <li>Konfirmasi pembayaran</li>
<li>Simpan bukti transaksi</li> <li>Simpan bukti transaksi</li>
</ol> </ol>
)}
</div> </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} ) : null}
{(!method || method === 'cstore') && (data.store || data.paymentCode) ? ( {(!method || method === 'cstore') && (data.store || data.paymentCode) ? (
@ -257,7 +297,7 @@ export function PaymentStatusPage() {
</div> </div>
</> </>
) : null} ) : 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 ? ( {qrSrc ? (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200"> <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"> <div className="text-sm text-gray-600 space-y-2">
<p className="font-medium text-gray-900">Langkah pembayaran:</p> <p className="font-medium text-gray-900">Langkah pembayaran:</p>
<ol className="list-decimal list-inside space-y-1 ml-2"> <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>Pilih menu Scan QR atau QRIS</li>
<li>Arahkan kamera ke QR code di atas</li> <li>Arahkan kamera ke QR code di atas</li>
<li>Konfirmasi pembayaran</li> <li>Konfirmasi pembayaran</li>
@ -295,6 +335,25 @@ export function PaymentStatusPage() {
</div> </div>
) : null} ) : 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} ) : null}
{(!method || method === 'credit_card') && data.maskedCard ? ( {(!method || method === 'credit_card') && data.maskedCard ? (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200"> <div className="bg-gray-50 rounded-lg p-4 border border-gray-200">

View File

@ -1,12 +1,12 @@
{ {
"mercant_id": "REFNO-003", "mercant_id": "REFNO-004",
"timestamp": 1733331600000, "timestamp": 1733331600000,
"deskripsi": "Bayar Internet", "deskripsi": "Bayar Internet",
"nominal": 250000, "nominal": 250000,
"nama": "Test User 3", "nama": "Test User 4",
"no_telepon": "081234567891", "no_telepon": "081234567892",
"email": "test3@example.com", "email": "test4@example.com",
"item": [ "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 }
] ]
} }