From 3512fa4a4d594a94e6f38d667e8d86b5094b1a93 Mon Sep 17 00:00:00 2001 From: CIFO Dev Date: Thu, 4 Dec 2025 14:56:53 +0700 Subject: [PATCH] fix ux --- server/index.cjs | 8 +- src/pages/CheckoutPage.tsx | 13 +- src/pages/PayPage.tsx | 21 ++- src/pages/PaymentStatusPage.tsx | 316 ++++++++++++++++++++++++-------- tmp-createtransaksi.json | 14 +- 5 files changed, 282 insertions(+), 90 deletions(-) diff --git a/server/index.cjs b/server/index.cjs index dcc0cac..08de767 100644 --- a/server/index.cjs +++ b/server/index.cjs @@ -73,7 +73,13 @@ const LOG_EXPOSE_API = parseEnable(process.env.LOG_EXPOSE_API) const LOG_BUFFER_SIZE = parseInt(process.env.LOG_BUFFER_SIZE || '1000', 10) const recentLogs = [] function shouldLog(level) { return (levelOrder[level] ?? 1) >= (levelOrder[LOG_LEVEL] ?? 1) } -function ts() { return new Date().toISOString() } +function ts() { + // Jakarta timezone (WIB = UTC+7) + const now = new Date() + const jakartaOffset = 7 * 60 // minutes + const localTime = new Date(now.getTime() + jakartaOffset * 60 * 1000) + return localTime.toISOString().replace('Z', '+07:00') +} function sanitize(obj) { try { return JSON.parse(JSON.stringify(obj)) } catch { return obj } } diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index ee9554e..cb55340 100644 --- a/src/pages/CheckoutPage.tsx +++ b/src/pages/CheckoutPage.tsx @@ -15,9 +15,10 @@ interface AutoSnapPaymentProps { onChargeInitiated?: () => void onSuccess?: (result: any) => void onError?: (error: any) => void + onModalClosed?: () => void } -function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Omit) { +function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError, onModalClosed }: Omit) { const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState('') const [paymentTriggered, setPaymentTriggered] = React.useState(false) @@ -105,6 +106,7 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Omit onClose: () => { Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId }) setLoading(false) + onModalClosed?.() // Enable status button when modal closed } }) @@ -137,7 +139,7 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Omit console.log('Cleanup: clearing timeout') clearTimeout(timer) } - }, [orderId, amount, customer, paymentTriggered, onSuccess, onError]) + }, [orderId, amount, customer, paymentTriggered, onSuccess, onError, onModalClosed]) // Don't render anything until we have valid data if (!orderId || !amount) { @@ -204,6 +206,7 @@ export function CheckoutPage() { const [selected, setSelected] = React.useState(null) const [currentStep, setCurrentStep] = React.useState<1 | 2>(1) const [isBusy, setIsBusy] = React.useState(false) + const [modalClosed, setModalClosed] = React.useState(false) const [form, setForm] = React.useState<{ name: string; contact: string; address: string; notes: string }>({ name: 'Demo User', contact: 'demo@example.com', @@ -243,7 +246,7 @@ export function CheckoutPage() { )} - + {/* Wizard 2 langkah: Step 1 (Form Dummy) β†’ Step 2 (Payment - Snap/Core auto-detect) */} {currentStep === 1 && (
@@ -328,8 +331,12 @@ export function CheckoutPage() { }} onError={(error) => { Logger.error('checkout.payment.error', { orderId, error }) + setModalClosed(true) // Enable status button on error // Handle payment error }} + onModalClosed={() => { + setModalClosed(true) // Enable status button when modal closed + }} />
)} diff --git a/src/pages/PayPage.tsx b/src/pages/PayPage.tsx index ff2ded5..9989a39 100644 --- a/src/pages/PayPage.tsx +++ b/src/pages/PayPage.tsx @@ -104,23 +104,30 @@ function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Auto Logger.paymentError('paypage.auto.snap.payment.error', { orderId, error: e.message }) console.error('[PayPage] Error:', e) - // Handle specific error: order_id already taken (payment already exists) + // Handle specific errors with user-friendly messages const errorMessage = e.response?.data?.message || e.message || '' - const isOrderTaken = errorMessage.includes('already been taken') || - errorMessage.includes('order_id has already been taken') + const errorMessages = e.response?.data?.error_messages || [] - if (isOrderTaken) { + // Check for "order_id already used" from Midtrans + const isOrderIdUsed = errorMessage.includes('sudah digunakan') || + errorMessage.includes('already been taken') || + errorMessage.includes('order_id has already been taken') || + errorMessages.some((msg: string) => msg.includes('sudah digunakan')) + + if (isOrderIdUsed) { // Order already has payment, redirect to status page Logger.paymentInfo('paypage.order.already_exists', { orderId }) console.log('[PayPage] Order already has payment, redirecting to status...') - // Show message briefly then redirect - setError('Pembayaran untuk order ini sudah dibuat. Mengalihkan ke halaman status...') + // Show user-friendly message then redirect + setError('Pembayaran untuk pesanan ini sudah dibuat sebelumnya. Anda akan diarahkan ke halaman status pembayaran...') setTimeout(() => { window.location.href = `/payments/${orderId}/status` }, 2000) } else { - setError(e.response?.data?.message || e.message || 'Gagal memuat pembayaran') + // Generic error with user-friendly message + const userMessage = 'Maaf, terjadi kesalahan saat memuat pembayaran. Silakan coba lagi atau hubungi customer service.' + setError(userMessage) } onError?.(e) diff --git a/src/pages/PaymentStatusPage.tsx b/src/pages/PaymentStatusPage.tsx index 0beade1..66f4ce1 100644 --- a/src/pages/PaymentStatusPage.tsx +++ b/src/pages/PaymentStatusPage.tsx @@ -15,6 +15,15 @@ export function PaymentStatusPage() { const method = (search.get('m') ?? undefined) as ('bank_transfer' | 'gopay' | 'qris' | 'cstore' | 'credit_card' | undefined) const { data, isLoading, error } = usePaymentStatus(orderId) + // Check if error is "transaction not found" from Midtrans + const errorData = (error as any)?.response?.data + const isTransactionNotFound = error && + (String(error).includes("doesn't exist") || + String(error).includes("404") || + String(error).includes("Transaction doesn't exist") || + errorData?.message?.includes("doesn't exist") || + errorData?.message?.includes("404")) + const statusText = data?.status ?? 'pending' const isFinal = ['settlement', 'capture', 'expire', 'cancel', 'deny', 'refund', 'chargeback'].includes(statusText) const isSuccess = statusText === 'settlement' || statusText === 'capture' @@ -89,87 +98,250 @@ export function PaymentStatusPage() { } } + // User-friendly status messages + function getStatusMessage(s: PaymentStatusResponse['status']) { + switch (s) { + case 'pending': + return { title: 'Menunggu Pembayaran', desc: 'Silakan selesaikan pembayaran Anda', icon: '⏳', color: 'yellow' } + case 'settlement': + case 'capture': + return { title: 'Pembayaran Berhasil', desc: 'Terima kasih! Pembayaran Anda telah dikonfirmasi', icon: 'βœ…', color: 'green' } + case 'deny': + return { title: 'Pembayaran Ditolak', desc: 'Maaf, pembayaran Anda ditolak. Silakan coba metode lain', icon: '❌', color: 'red' } + case 'cancel': + return { title: 'Pembayaran Dibatalkan', desc: 'Transaksi telah dibatalkan', icon: '🚫', color: 'red' } + case 'expire': + return { title: 'Pembayaran Kedaluwarsa', desc: 'Waktu pembayaran habis. Silakan buat transaksi baru', icon: '⏰', color: 'red' } + case 'refund': + return { title: 'Pembayaran Dikembalikan', desc: 'Dana telah dikembalikan ke rekening Anda', icon: '↩️', color: 'blue' } + default: + return { title: 'Status Tidak Diketahui', desc: 'Hubungi customer service untuk bantuan', icon: 'ℹ️', color: 'gray' } + } + } + + const statusMsg = getStatusMessage(statusText) + return ( -
-

Status Pembayaran

-
-
Order ID: {orderId}
- {method || data?.method ? ( -
Metode: {data?.method ?? method}
- ) : null} -
Status: {isLoading ? ( - memuat… - ) : error ? ( - gagal memuat - ) : ( - {statusText} - )}
-
- {isFinal ? 'Status final β€” polling dihentikan.' : 'Polling setiap 3 detik hingga status final.'} -
- {isSuccess ? ( -
-
βœ… Pembayaran Berhasil!
- +
+
+ {/* Header Card */} +
+
+ {isLoading ? ( + <> +
⏳
+
Memuat status...
+
Mohon tunggu sebentar
+ + ) : isTransactionNotFound ? ( + <> +
πŸ“‹
+
Transaksi Belum Dibuat
+
Silakan kembali ke halaman checkout untuk membuat pembayaran
+ + ) : error ? ( + <> +
⚠️
+
Gagal Memuat Status
+
Terjadi kesalahan. Silakan refresh halaman
+ + ) : ( + <> +
{statusMsg.icon}
+
{statusMsg.title}
+
{statusMsg.desc}
+ + )}
- ) : null} - {/* Method-specific details */} - {!isLoading && !error && data ? ( -
- {(!method || method === 'bank_transfer') && data.vaNumber ? ( -
-
Virtual Account
-
VA Number: {data.vaNumber}
- {data.bank ?
Bank: {data.bank.toUpperCase()}
: null} - {data.billKey && data.billerCode ? ( -
Mandiri E-Channel β€” Bill Key: {data.billKey}, Biller: {data.billerCode}
- ) : null} + + {/* Order Info */} +
+
+
+
ID Pesanan
+
{orderId}
- ) : null} - {(!method || method === 'cstore') && (data.store || data.paymentCode) ? ( -
-
Convenience Store
- {data.store ?
Store: {data.store}
: null} - {data.paymentCode ?
Payment Code: {data.paymentCode}
: null} -
- ) : null} - {(!method || method === 'gopay' || method === 'qris') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? ( -
-
QR / Deeplink
- {qrSrc ? ( -
- QR untuk pembayaran { - const next = qrCandidates.find((u) => u !== e.currentTarget.src) - if (next) setQrSrc(next) - }} /> -
- ) : ( -
Gunakan link berikut untuk membuka aplikasi pembayaran.
- )} -
- {(Array.isArray(data?.actions) ? data!.actions : []).map((a, i) => ( - - {a.name || a.method || 'Buka'} - - ))} + {!isLoading && !isFinal && !isTransactionNotFound && ( +
+
+ Memperbarui otomatis...
-
- ) : null} - {(!method || method === 'credit_card') && data.maskedCard ? ( -
-
Kartu
-
Masked Card: {data.maskedCard}
+ )} +
+ + {method || data?.method ? ( +
+
Metode Pembayaran
+
{(data?.method ?? method)?.replace('_', ' ')}
) : null}
+
+ + {isSuccess ? ( +
+
+
πŸŽ‰
+
+
Transaksi Selesai!
+
Anda akan diarahkan ke halaman utama dalam beberapa detik
+ +
+
+
) : null} - {/* Aksi bawah dihilangkan sesuai permintaan */} + {/* Payment Instructions - Only show for pending status */} + {!isLoading && !error && data && statusText === 'pending' ? ( +
+
+
πŸ“ Cara Pembayaran
+
+
+ {(!method || method === 'bank_transfer') && data.vaNumber ? ( + <> +
+
Nomor Virtual Account
+
+
{data.vaNumber}
+ +
+ {data.bank ? ( +
+
Bank
+
{data.bank}
+
+ ) : null} +
+
+

Langkah pembayaran:

+
    +
  1. Buka aplikasi mobile banking atau ATM
  2. +
  3. Pilih menu Transfer / Bayar
  4. +
  5. Masukkan nomor Virtual Account di atas
  6. +
  7. Konfirmasi pembayaran
  8. +
  9. Simpan bukti transaksi
  10. +
+
+ {data.billKey && data.billerCode ? ( +
+
Khusus Mandiri E-Channel:
+
Kode Biller: {data.billerCode}
+
Kode Bayar: {data.billKey}
+
+ ) : null} + + ) : null} + {(!method || method === 'cstore') && (data.store || data.paymentCode) ? ( + <> +
+ {data.store ? ( +
+
Toko
+
{data.store}
+
+ ) : null} + {data.paymentCode ? ( + <> +
Kode Pembayaran
+
+
{data.paymentCode}
+ +
+ + ) : null} +
+
+

Langkah pembayaran:

+
    +
  1. Kunjungi toko {data.store || 'convenience store'} terdekat
  2. +
  3. Berikan kode pembayaran kepada kasir
  4. +
  5. Lakukan pembayaran tunai
  6. +
  7. Simpan bukti pembayaran
  8. +
+
+ + ) : null} + {(!method || method === 'gopay' || method === 'qris') && (qrSrc || (Array.isArray(data?.actions) && data.actions.length > 0)) ? ( + <> + {qrSrc ? ( +
+
Scan QR Code
+
+ QR Code Pembayaran { + const next = qrCandidates.find((u) => u !== e.currentTarget.src) + if (next) setQrSrc(next) + }} /> +
+
+ ) : null} +
+

Langkah pembayaran:

+
    +
  1. Buka aplikasi {method === 'gopay' ? 'GoPay/Gojek' : 'e-wallet atau m-banking'}
  2. +
  3. Pilih menu Scan QR atau QRIS
  4. +
  5. Arahkan kamera ke QR code di atas
  6. +
  7. Konfirmasi pembayaran
  8. +
+
+ {(Array.isArray(data?.actions) && data.actions.length > 0) ? ( +
+ {data.actions.map((a, i) => ( + + πŸ“± {a.name || 'Buka Aplikasi'} + + ))} +
+ ) : null} + + ) : null} + {(!method || method === 'credit_card') && data.maskedCard ? ( +
+
Kartu Kredit/Debit
+
{data.maskedCard}
+
+ Pembayaran dengan kartu telah diproses. Tunggu konfirmasi dari bank Anda. +
+
+ ) : null} +
+
+ ) : null} + {/* Help Section */} + {!isLoading && !error && ( +
+
+

πŸ’‘ Butuh bantuan?

+
    +
  • β€’ Jika pembayaran belum terkonfirmasi dalam 24 jam, hubungi customer service
  • +
  • β€’ Simpan nomor pesanan untuk referensi
  • +
  • β€’ Halaman ini akan diperbarui otomatis saat status berubah
  • +
+
+
+ )}
- {!Env.API_BASE_URL && ( - - Tambahkan VITE_API_BASE_URL di env agar status memuat dari backend; saat ini menggunakan stub. - - )}
) } \ No newline at end of file diff --git a/tmp-createtransaksi.json b/tmp-createtransaksi.json index c937e8d..cde8748 100644 --- a/tmp-createtransaksi.json +++ b/tmp-createtransaksi.json @@ -1,12 +1,12 @@ ο»Ώ{ - "mercant_id": "REFNO-002", - "timestamp": 1733283600000, + "mercant_id": "REFNO-003", + "timestamp": 1733331600000, "deskripsi": "Bayar Internet", - "nominal": 200000, - "nama": "Demo User 2", - "no_telepon": "081234567890", - "email": "demo2@example.com", + "nominal": 250000, + "nama": "Test User 3", + "no_telepon": "081234567891", + "email": "test3@example.com", "item": [ - { "item_id": "TKG-2512041", "nama": "Internet Desember", "harga": 200000, "qty": 1 } + { "item_id": "TKG-2512042", "nama": "Internet Desember Premium", "harga": 250000, "qty": 1 } ] }