-
Virtual Account
-
- {vaCode ? (
-
- Nomor VA:
- {vaCode}
-
- ) : (
-
- {busy && }
- {busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
-
- )}
- {billKey && (
- Bill Key: {billKey}
- )}
- {billerCode && (
- Biller Code: {billerCode}
- )}
-
-
-
-
+ <>
+
+
+
Transfer Bank
+ {selected && (
+
+ Bank:
+ {selected.toUpperCase()}
+
+ )}
+
VA dibuat otomatis sesuai bank pilihan Anda.
+ {errorMessage && (
+
{errorMessage}
+ )}
+ {selected && (
+
+
+
Virtual Account
+
+ {vaCode ? (
+
+ Nomor VA:
+ {vaCode}
+
+ ) : (
+
+ {busy && }
+ {busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
+
+ )}
+ {billKey && (
+ Bill Key: {billKey}
+ )}
+ {billerCode && (
+ Biller Code: {billerCode}
+ )}
+
+
+
+
+
-
- )}
- {/* Status inline dengan polling otomatis */}
- {selected && (
-
- )}
- {selected && (
-
- {selected === 'bca' ? (
-
- ) : (
-
-
Instruksi pembayaran
-
-
- )}
-
- )}
- {locked && (
-
Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.
- )}
-
- {(!vaCode || errorMessage) && (
-
-
+ >
)
}
diff --git a/src/features/payments/components/GoPayPanel.tsx b/src/features/payments/components/GoPayPanel.tsx
index 38bb405..680b572 100644
--- a/src/features/payments/components/GoPayPanel.tsx
+++ b/src/features/payments/components/GoPayPanel.tsx
@@ -73,16 +73,16 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
setMode('gopay')}
- aria-pressed={mode==='gopay'}
- className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode==='gopay' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
+ aria-pressed={mode === 'gopay'}
+ className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode === 'gopay' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
>
GoPay
setMode('qris')}
- aria-pressed={mode==='qris'}
- className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
+ aria-pressed={mode === 'qris'}
+ className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode === 'qris' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`}
>
QRIS
@@ -135,7 +135,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
className="w-full"
aria-busy={busy}
disabled={busy || (!locked && actions.length === 0)}
- onClick={() => { setBusy(true); onChargeInitiated?.(); setTimeout(() => { nav.toStatus(orderId, mode) ; setBusy(false) }, 250) }}
+ onClick={() => { setBusy(true); onChargeInitiated?.(); setTimeout(() => { nav.toStatus(orderId, mode); setBusy(false) }, 250) }}
>
{busy ? (
@@ -216,7 +216,7 @@ export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy,
}
run()
return () => { cancelled = true }
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode])
return null
}
\ No newline at end of file
diff --git a/src/lib/errorMessages.ts b/src/lib/errorMessages.ts
new file mode 100644
index 0000000..8a4bcca
--- /dev/null
+++ b/src/lib/errorMessages.ts
@@ -0,0 +1,81 @@
+import type { AxiosError } from 'axios'
+
+/**
+ * Maps technical error responses to user-friendly messages in Bahasa Indonesia
+ * for non-tech-savvy users (ibu-ibu awam)
+ */
+export function mapErrorToUserMessage(error: unknown): string {
+ // Handle AxiosError
+ if (error && typeof error === 'object' && 'response' in error) {
+ const axiosError = error as AxiosError
+ const status = axiosError.response?.status
+
+ // HTTP 409 - Conflict (VA/QR/Code already created)
+ if (status === 409) {
+ return 'Kode pembayaran Anda sudah dibuat! Silakan gunakan kode yang sudah ada.'
+ }
+
+ // HTTP 404 - Not Found
+ if (status === 404) {
+ return 'Terjadi kesalahan. Silakan coba lagi.'
+ }
+
+ // HTTP 500 - Internal Server Error
+ if (status === 500) {
+ return 'Terjadi kesalahan server. Silakan coba lagi nanti.'
+ }
+
+ // HTTP 503 - Service Unavailable
+ if (status === 503) {
+ return 'Layanan sedang sibuk. Silakan coba lagi dalam beberapa saat.'
+ }
+
+ // Network error (no response)
+ if (axiosError.message === 'Network Error' || !axiosError.response) {
+ return 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.'
+ }
+
+ // Try to get message from response data
+ const responseMessage = (axiosError.response?.data as any)?.message
+ if (typeof responseMessage === 'string' && responseMessage.length > 0) {
+ // If it's already in Indonesian, use it
+ if (/[a-zA-Z]/.test(responseMessage) === false) {
+ return responseMessage
+ }
+ }
+ }
+
+ // Handle Error object
+ if (error instanceof Error) {
+ // Network errors
+ if (error.message.includes('Network') || error.message.includes('network')) {
+ return 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.'
+ }
+
+ // Timeout errors
+ if (error.message.includes('timeout') || error.message.includes('Timeout')) {
+ return 'Permintaan memakan waktu terlalu lama. Silakan coba lagi.'
+ }
+ }
+
+ // Default fallback message
+ return 'Terjadi kesalahan. Silakan coba lagi.'
+}
+
+/**
+ * Gets recovery action suggestion based on error type
+ */
+export function getErrorRecoveryAction(error: unknown): 'retry' | 'view-existing' | 'back' {
+ if (error && typeof error === 'object' && 'response' in error) {
+ const axiosError = error as AxiosError
+ const status = axiosError.response?.status
+
+ // HTTP 409 - Conflict (already exists) → view existing
+ if (status === 409) {
+ return 'view-existing'
+ }
+ }
+
+ // Default: allow retry
+ return 'retry'
+}