Compare commits
No commits in common. "a9bb11f080be2cc83735342c5d0752e3139c85d4" and "01179d646ee99e93dd288240d31fbcdd2f329bc4" have entirely different histories.
a9bb11f080
...
01179d646e
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue