feat: improve payments UX (VA layout, instructions, accessibility)
BankTransferPanel: VA card above instructions; clearer borders; aria-live for VA status; dynamic 'Instruksi pembayaran' panel.\nBcaInstructionList: ARIA roles for tablist/tabs/tabpanels; stronger visual cues; 3px focus ring.\nGoPayPanel: auto display instructions with dynamic steps per mode; clearer mode toggle buttons; 3px focus ring.\nButton: standardize 3px focus ring with 2px offset and accent color.\nPaymentMethodList: enhanced contrast, thicker borders, larger targets for seniors.\nPaymentInstructions: flexible title and steps; never empty with sensible fallback.
This commit is contained in:
parent
4862c32978
commit
343afa2af9
|
|
@ -0,0 +1,264 @@
|
||||||
|
# Core Midtrans CIFO UI/UX Specification
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
- Improve legibility and contrast for older users by strengthening color, typography, and separators across Checkout flows (QRIS, GoPay, Convenience Store, Bank Transfer).
|
||||||
|
- Establish clear theme tokens so implementation stays consistent across components.
|
||||||
|
|
||||||
|
Change Log
|
||||||
|
- 2025-11-10: Initial draft focused on contrast, typography, and card/divider clarity.
|
||||||
|
|
||||||
|
Theme Foundations
|
||||||
|
- Color Roles (recommended hex values):
|
||||||
|
- Page background: `#F7FAFC` (very light slate)
|
||||||
|
- Surface/background (cards, panels): `#FFFFFF`
|
||||||
|
- Text primary: `#0B1020` (near-black for high contrast)
|
||||||
|
- Text secondary: `#374151` (medium slate; still readable)
|
||||||
|
- Border default: `#94A3B8` (slate 400)
|
||||||
|
- Border strong: `#64748B` (slate 500)
|
||||||
|
- Accent/primary: `#2563EB` (blue 600)
|
||||||
|
- Success: `#16A34A` (green 600)
|
||||||
|
- Danger: `#DC2626` (red 600)
|
||||||
|
- Warning: `#D97706` (amber 600)
|
||||||
|
- Info: `#0EA5E9` (sky 500)
|
||||||
|
- Focus and States:
|
||||||
|
- Focus ring color: `#2563EB`
|
||||||
|
- Focus ring width: `3px`
|
||||||
|
- Hover/active states should increase contrast by at least one shade (e.g., blue 600 → blue 700 on hover).
|
||||||
|
- Typography:
|
||||||
|
- Font stack (sans): `Inter, Segoe UI, system-ui, -apple-system, Roboto, Noto Sans, Arial, sans-serif`
|
||||||
|
- Base body size: `16px` minimum; labels `15px`; buttons `16px`
|
||||||
|
- Headings: `H4 20px`, `H3 24px`, `H2 28px` (line-height ~1.3)
|
||||||
|
- Body line-height: `1.5` for readability
|
||||||
|
- Avoid ultra-light weights; prefer `400–600` for body/labels
|
||||||
|
- Separators and Borders:
|
||||||
|
- Default divider: `1.5px` `#94A3B8`
|
||||||
|
- Strong divider: `2px` `#64748B` for section breaks and card boundaries
|
||||||
|
- Cards: visible 2px border, subtle shadow `0 1px 2px rgba(0,0,0,0.08)`
|
||||||
|
- Spacing Scale (base):
|
||||||
|
- `4px` units; common spacing: `8/12/16/24/32px`
|
||||||
|
- Minimum interior padding for cards: `16–24px`
|
||||||
|
|
||||||
|
Accessibility Standards
|
||||||
|
- Contrast:
|
||||||
|
- Normal text contrast ≥ `4.5:1`; large text (≥18px) ≥ `3:1`
|
||||||
|
- Verify accent on white and on light surfaces meets ratios
|
||||||
|
- Touch Targets:
|
||||||
|
- Minimum hit area: `44x44px` for interactive elements
|
||||||
|
- Minimum tap spacing: `8px` around grouped actions
|
||||||
|
- Focus & Keyboard:
|
||||||
|
- Always-visible focus outline with `3px` ring in accent color
|
||||||
|
- Skip links available on long forms
|
||||||
|
- Motion & Feedback:
|
||||||
|
- Respect reduced motion; avoid aggressive animations
|
||||||
|
- Provide textual or icon feedback in addition to color changes
|
||||||
|
|
||||||
|
Component Guidance
|
||||||
|
- Cards/Panels:
|
||||||
|
- Use strong border (`2px #64748B`) for outer frame when content density is high
|
||||||
|
- Title uses `H4/H3` with primary text color; subtitle uses secondary
|
||||||
|
- Interior dividers use default border (`1.5px #94A3B8`)
|
||||||
|
- Buttons:
|
||||||
|
- Primary: solid `#2563EB` with white text; hover `#1D4ED8`
|
||||||
|
- Secondary: outline with strong border and primary text
|
||||||
|
- Disabled: `#CBD5E1` background, `#64748B` text; maintain contrast
|
||||||
|
- Focus: `3px` accent ring outside button
|
||||||
|
- Inputs:
|
||||||
|
- Label size `15–16px` and primary text color; never rely on placeholder as label
|
||||||
|
- Field border default; on focus switch to strong border and accent ring
|
||||||
|
- Error state uses danger color and text hint; icon optional
|
||||||
|
- Lists/Selection (e.g., store selection):
|
||||||
|
- Row height ≥ `48px`; radio/checkbox minimum `24px`
|
||||||
|
- Selected state uses strong border and light accent background (`blue-50`)
|
||||||
|
|
||||||
|
Implementation Notes
|
||||||
|
- CSS Variables (define once):
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--color-bg-page: #F7FAFC;
|
||||||
|
--color-bg-surface: #FFFFFF;
|
||||||
|
--color-text-primary: #0B1020;
|
||||||
|
--color-text-secondary: #374151;
|
||||||
|
--color-border-default: #94A3B8;
|
||||||
|
--color-border-strong: #64748B;
|
||||||
|
--color-accent: #2563EB;
|
||||||
|
--color-success: #16A34A;
|
||||||
|
--color-danger: #DC2626;
|
||||||
|
--color-warning: #D97706;
|
||||||
|
--color-info: #0EA5E9;
|
||||||
|
--focus-ring-width: 3px;
|
||||||
|
--focus-ring-color: #2563EB;
|
||||||
|
--shadow-card: 0 1px 2px rgba(0,0,0,0.08);
|
||||||
|
--border-width-default: 1.5px;
|
||||||
|
--border-width-strong: 2px;
|
||||||
|
--font-family-sans: Inter, Segoe UI, system-ui, -apple-system, Roboto, Noto Sans, Arial, sans-serif;
|
||||||
|
--font-size-body: 16px;
|
||||||
|
--font-size-label: 15px;
|
||||||
|
--font-size-button: 16px;
|
||||||
|
--font-size-h4: 20px;
|
||||||
|
--font-size-h3: 24px;
|
||||||
|
--font-size-h2: 28px;
|
||||||
|
--line-height-body: 1.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Tailwind Theme (high-level):
|
||||||
|
- Map CSS variables to custom colors via CSS-in-JS or add a custom palette in `tailwind.config.*` aligned to the hex values above
|
||||||
|
- Increase `ringWidth` default to `3`, add `ringColor` for focus, and define `borderWidth` scale with `1.5` and `2`
|
||||||
|
- Component Application:
|
||||||
|
- Replace `border-gray-200/300` with `border-[#94A3B8]` for dividers
|
||||||
|
- Use `border-[2px]` + `border-[#64748B]` for card frames when needed
|
||||||
|
- Set `text-[#0B1020]` as default text and avoid pure black for softer rendering
|
||||||
|
|
||||||
|
Validation Checklist
|
||||||
|
- Body text ≥16px and headings per spec
|
||||||
|
- Divider visibility validated on standard laptop and low-contrast displays
|
||||||
|
- All critical actions meet contrast and focus outline requirements
|
||||||
|
- Component states (hover, active, disabled, error) meet color and contrast standards
|
||||||
|
|
||||||
|
Instruksi Langkah Pembayaran
|
||||||
|
- Tujuan: Menyajikan cara bayar yang jelas tanpa Step Wizard. Fokus pada daftar langkah ringkas untuk tiap metode (QRIS, GoPay, Transfer Bank via Mobile/Internet Banking/ATM, Convenience Store).
|
||||||
|
- Prinsip Penyajian:
|
||||||
|
- Judul panel: "Cara Bayar" + nama metode/bank (mis. "Cara Bayar: BCA Mobile", "Cara Bayar: ATM BNI", "Cara Bayar: QRIS").
|
||||||
|
- Gunakan daftar bernomor (1., 2., 3.) dengan jarak antar langkah `12–16px` dan body `16–18px`.
|
||||||
|
- Kalimat singkat, langsung, maksimal ±100 karakter per langkah.
|
||||||
|
- Gunakan teks primer `#0B1020`; hindari abu-abu pucat untuk keterbacaan.
|
||||||
|
- Divider opsional antar kelompok langkah memakai border default `#94A3B8`.
|
||||||
|
- Ikon langkah opsional; jangan menggantikan teks instruksi.
|
||||||
|
|
||||||
|
Pola per Metode
|
||||||
|
- QRIS
|
||||||
|
- 1. Buka aplikasi e-wallet/mbanking yang mendukung QRIS.
|
||||||
|
- 2. Arahkan kamera ke QR di layar.
|
||||||
|
- 3. Periksa detail pembayaran dan konfirmasi.
|
||||||
|
- 4. Selesai. Simpan bukti transaksi.
|
||||||
|
- Catatan: tampilkan countdown kedaluwarsa dan state "QR kedaluwarsa" dengan aksi "Buat Ulang" jika diperlukan.
|
||||||
|
|
||||||
|
- GoPay
|
||||||
|
- 1. Ketuk tombol "Buka GoPay" (atau buka aplikasi GoPay manual).
|
||||||
|
- 2. Periksa detail pembayaran.
|
||||||
|
- 3. Konfirmasi dan selesaikan pembayaran.
|
||||||
|
- 4. Simpan bukti transaksi.
|
||||||
|
|
||||||
|
- Transfer Bank via Mobile Banking (contoh umum)
|
||||||
|
- 1. Buka aplikasi mobile banking dan login.
|
||||||
|
- 2. Pilih menu "Transfer" → "Virtual Account".
|
||||||
|
- 3. Masukkan nomor VA dan nominal.
|
||||||
|
- 4. Periksa detail, konfirmasi dengan PIN/OTP.
|
||||||
|
- 5. Simpan bukti transaksi.
|
||||||
|
|
||||||
|
- Transfer Bank via Internet Banking (contoh umum)
|
||||||
|
- 1. Buka situs internet banking dan login.
|
||||||
|
- 2. Pilih menu "Transfer" → "Virtual Account".
|
||||||
|
- 3. Masukkan nomor VA dan nominal.
|
||||||
|
- 4. Periksa detail, konfirmasi dengan OTP.
|
||||||
|
- 5. Simpan bukti transaksi.
|
||||||
|
|
||||||
|
- Transfer Bank via ATM (contoh umum)
|
||||||
|
- 1. Kunjungi ATM dan masukkan kartu, pilih bahasa.
|
||||||
|
- 2. Pilih menu "Transfer" → "Virtual Account".
|
||||||
|
- 3. Masukkan nomor VA dan nominal.
|
||||||
|
- 4. Periksa detail, konfirmasi.
|
||||||
|
- 5. Ambil dan simpan struk transaksi.
|
||||||
|
|
||||||
|
- Convenience Store (Alfamart/Indomaret)
|
||||||
|
- 1. Kunjungi toko yang dipilih (Alfamart/Indomaret).
|
||||||
|
- 2. Tunjukkan "Kode Pembayaran" kepada kasir.
|
||||||
|
- 3. Lakukan pembayaran sebelum kedaluwarsa.
|
||||||
|
- 4. Simpan struk. Kunjungi halaman "Cek Status Pembayaran" jika diperlukan.
|
||||||
|
|
||||||
|
Copywriting Baku
|
||||||
|
- Gunakan label konsisten: "Kode Pembayaran", "Salin Kode", "Cek Status Pembayaran", "Buka GoPay", "QRIS", "Kedaluwarsa dalam", "Buat Ulang".
|
||||||
|
- Bahasa Indonesia, kalimat aktif, hindari istilah teknis berlebihan.
|
||||||
|
- Pastikan penulisan brand dan bank sesuai pedoman resmi.
|
||||||
|
|
||||||
|
Aksesibilitas & Lansia
|
||||||
|
- Body `16–18px`, line-height `1.5`; jarak antar langkah `12–16px`.
|
||||||
|
- Kontras AA untuk teks dan divider; fokus `ring 3px` selalu terlihat.
|
||||||
|
- Comfort Mode opsional: font `18px`, border kuat `3px`, spacing +20%.
|
||||||
|
|
||||||
|
Mobile UX — Pembayaran
|
||||||
|
- Tata letak:
|
||||||
|
- Satu kolom pada lebar kecil; gunakan container `max-w-md`.
|
||||||
|
- Spasi internal panel `16–24px`; hindari konten menempel ke tepi.
|
||||||
|
- Target sentuh:
|
||||||
|
- Tinggi area tap minimal `44px`; jarak antar aksi `8–12px`.
|
||||||
|
- Baris pemilihan metode memakai `min-h-[44px]` dan `p-3`.
|
||||||
|
- Tipografi:
|
||||||
|
- Body `16px` (min); langkah instruksi `16–18px` dengan `line-height 1.5`.
|
||||||
|
- Hindari `text-xs` untuk konten utama di mobile.
|
||||||
|
- Aksi utama:
|
||||||
|
- Tombol utama `w-full`; boleh sticky di bawah layar.
|
||||||
|
- Pertimbangkan padding aman: `padding-bottom: env(safe-area-inset-bottom)`.
|
||||||
|
- Panel QR/Code:
|
||||||
|
- QR minimum `min(68vw, 280px)`; grid pusat; border jelas.
|
||||||
|
- Kode pembayaran memakai `font-mono`, `text-lg`, `letter-spacing ~0.06em`, `select-all`.
|
||||||
|
- Tombol “Salin Kode” `w-full` di mobile.
|
||||||
|
- Loading & status:
|
||||||
|
- Pakai teks pendamping pada spinner; tambahkan `aria-live="polite"`.
|
||||||
|
- Tampilkan countdown kedaluwarsa dan aksi “Buat Ulang” jika relevan.
|
||||||
|
- Performa:
|
||||||
|
- Lazy-load gambar brand/QR; batasi ukuran logo; cache aktif.
|
||||||
|
- Hormati `prefers-reduced-motion`.
|
||||||
|
|
||||||
|
Implementasi Komponen (Opsional)
|
||||||
|
- InstructionList API:
|
||||||
|
```ts
|
||||||
|
type InstructionListProps = {
|
||||||
|
title: string; // "Cara Bayar: QRIS", "Cara Bayar: BCA Mobile"
|
||||||
|
steps: string[]; // daftar langkah pendek
|
||||||
|
footnote?: string; // catatan opsional (mis. kedaluwarsa)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Gaya default:
|
||||||
|
- Judul `H4/H3` dengan teks primer; langkah bernomor dengan spacing `12–16px`.
|
||||||
|
- Divider antar kelompok langkah memakai `#94A3B8`; tidak wajib.
|
||||||
|
- Ikon kecil opsional di kiri; jangan menggantikan teks.
|
||||||
|
|
||||||
|
Bank Spesifik: BCA
|
||||||
|
- BCA Mobile (m-BCA) — Virtual Account
|
||||||
|
- 1. Buka aplikasi BCA mobile dan login.
|
||||||
|
- 2. Pilih menu "m-BCA" → "m-Transfer".
|
||||||
|
- 3. Pilih "BCA Virtual Account".
|
||||||
|
- 4. Masukkan "Nomor VA" yang tertera di halaman pembayaran.
|
||||||
|
- 5. Masukkan nominal jika tidak terisi otomatis.
|
||||||
|
- 6. Periksa detail pembayaran, lalu ketuk "Send".
|
||||||
|
- 7. Masukkan PIN m-BCA untuk konfirmasi.
|
||||||
|
- 8. Simpan bukti transaksi.
|
||||||
|
- Catatan: Nama menu dapat berbeda pada versi aplikasi tertentu; sesuaikan jika perlu.
|
||||||
|
|
||||||
|
- ATM BCA — Virtual Account
|
||||||
|
- 1. Masukkan kartu BCA dan PIN.
|
||||||
|
- 2. Pilih "Transaksi Lainnya".
|
||||||
|
- 3. Pilih "Transfer".
|
||||||
|
- 4. Pilih "Ke BCA Virtual Account".
|
||||||
|
- 5. Masukkan "Nomor VA" yang tertera di halaman pembayaran.
|
||||||
|
- 6. Masukkan nominal jika diminta.
|
||||||
|
- 7. Periksa detail pembayaran, lalu konfirmasi.
|
||||||
|
- 8. Ambil dan simpan struk transaksi.
|
||||||
|
- Catatan: Urutan menu pada beberapa ATM bisa berbeda; gunakan opsi Virtual Account ketika tersedia.
|
||||||
|
|
||||||
|
- Internet Banking (KlikBCA) — Virtual Account
|
||||||
|
- 1. Buka situs resmi KlikBCA dan login dengan user ID.
|
||||||
|
- 2. Pilih menu "Transfer Dana".
|
||||||
|
- 3. Pilih "Ke BCA Virtual Account".
|
||||||
|
- 4. Masukkan "Nomor VA" yang tertera di halaman pembayaran.
|
||||||
|
- 5. Masukkan nominal jika diperlukan.
|
||||||
|
- 6. Periksa detail pembayaran, lalu konfirmasi.
|
||||||
|
- 7. Masukkan OTP/KeyBCA (token) untuk menyetujui transaksi.
|
||||||
|
- 8. Simpan bukti transaksi.
|
||||||
|
- Catatan: Nama menu dapat berbeda antar versi; pastikan memilih opsi Virtual Account.
|
||||||
|
|
||||||
|
Copywriting Baku — Tambahan
|
||||||
|
- Untuk metode Virtual Account, gunakan label "Nomor VA" secara konsisten pada UI dan instruksi.
|
||||||
|
|
||||||
|
Roadmap Implementasi — Mobile Pembayaran (10 TODO)
|
||||||
|
- [x] Tambah panduan "Mobile UX — Pembayaran" di spesifikasi.
|
||||||
|
- [x] Perbesar area sentuh PaymentMethodList (min height ≥44px).
|
||||||
|
- [x] Tingkatkan keterbacaan PaymentInstructions (text-sm, spacing yang cukup).
|
||||||
|
- [x] Perbesar keterbacaan Kode Pembayaran di CStore (font-mono, text-lg, tracking, select-all).
|
||||||
|
- [x] Perbesar kontainer QR GoPay/QRIS dan buat tombol aksi full-width di mobile.
|
||||||
|
- [ ] PaymentSheet: optimalkan header mobile dan countdown jelas.
|
||||||
|
- Catatan: tombol utama sticky di bawah sudah diimplementasi.
|
||||||
|
- [x] BankTransferPanel: implementasi komponen InstructionList untuk BCA (Mobile/ATM/KlikBCA).
|
||||||
|
- [ ] Aksesibilitas: standarisasi aria-live untuk spinner/QR/kode, fokus ring 3px di semua panel.
|
||||||
|
- [ ] Comfort Mode: tambah toggle (font 18px, border 3px, spacing +20%) dan token gaya terkait.
|
||||||
|
- [ ] QA lintas perangkat: uji di layar kecil (iPhone SE/Android kecil), sesuaikan token bila perlu.
|
||||||
|
|
@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { cn } from '../../lib/cn'
|
import { cn } from '../../lib/cn'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:focus-ring disabled:opacity-50 disabled:pointer-events-none',
|
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-black disabled:opacity-50 disabled:pointer-events-none',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Button } from '../../../components/ui/button'
|
||||||
import { usePaymentNavigation } from '../lib/navigation'
|
import { usePaymentNavigation } from '../lib/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PaymentInstructions } from './PaymentInstructions'
|
import { PaymentInstructions } from './PaymentInstructions'
|
||||||
|
import { BcaInstructionList } from './BcaInstructionList'
|
||||||
import { TrustStrip } from './TrustStrip'
|
import { TrustStrip } from './TrustStrip'
|
||||||
import { BankLogosRow, BankLogo, type BankKey } from './PaymentLogos'
|
import { BankLogosRow, BankLogo, type BankKey } from './PaymentLogos'
|
||||||
import { postCharge } from '../../../services/api'
|
import { postCharge } from '../../../services/api'
|
||||||
|
|
@ -126,6 +127,13 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selected])
|
}, [selected])
|
||||||
|
|
||||||
|
// Auto-show instructions when BCA is selected to reduce confusion
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (selected === 'bca' && !showGuide) {
|
||||||
|
setShowGuide(true)
|
||||||
|
}
|
||||||
|
}, [selected])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="font-medium">Transfer bank</div>
|
<div className="font-medium">Transfer bank</div>
|
||||||
|
|
@ -137,43 +145,49 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
<span className="text-black/80 dark:text-white/80 font-semibold">{selected.toUpperCase()}</span>
|
<span className="text-black/80 dark:text-white/80 font-semibold">{selected.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-black/60 dark:text-white/60">VA dibuat otomatis sesuai bank pilihan Anda.</div>
|
<div className="text-sm text-black/70 dark:text-white/70">VA dibuat otomatis sesuai bank pilihan Anda.</div>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert title="Gagal membuat VA">{errorMessage}</Alert>
|
<Alert title="Gagal membuat VA">{errorMessage}</Alert>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowGuide((v) => !v)}
|
|
||||||
className="text-xs text-brand-600 hover:underline"
|
|
||||||
aria-expanded={showGuide}
|
|
||||||
>
|
|
||||||
Cara bayar
|
|
||||||
</button>
|
|
||||||
{showGuide && <PaymentInstructions method="bank_transfer" />}
|
|
||||||
{selected && (
|
{selected && (
|
||||||
<div className="flex flex-col gap-3 pt-1">
|
<div className="pt-1">
|
||||||
<div className="text-sm text-black/60 dark:text-white/60">
|
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20">
|
||||||
{vaCode ? (
|
<div className="text-sm font-medium mb-2">Virtual Account</div>
|
||||||
<span>VA: <span className="font-mono text-2xl md:text-3xl font-semibold tracking-wider text-black dark:text-white">{vaCode}</span></span>
|
<div className="text-sm text-black/70 dark:text-white/70">
|
||||||
) : (
|
{vaCode ? (
|
||||||
<span className="inline-flex items-center gap-2">
|
<span>Nomor VA: <span className="font-mono text-2xl md:text-3xl font-semibold tracking-wider text-black dark:text-white">{vaCode}</span></span>
|
||||||
{busy && <span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />}
|
) : (
|
||||||
{busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
|
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
|
||||||
</span>
|
{busy && <span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />}
|
||||||
)}
|
{busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'}
|
||||||
{billKey && (
|
</span>
|
||||||
<span className="ml-3">Bill Key: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billKey}</span></span>
|
)}
|
||||||
)}
|
{billKey && (
|
||||||
{billerCode && (
|
<span className="ml-3">Bill Key: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billKey}</span></span>
|
||||||
<span className="ml-3">Biller Code: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billerCode}</span></span>
|
)}
|
||||||
)}
|
{billerCode && (
|
||||||
</div>
|
<span className="ml-3">Biller Code: <span className="font-mono text-lg font-semibold text-black dark:text-white">{billerCode}</span></span>
|
||||||
<div className="flex gap-2">
|
)}
|
||||||
<Button variant="outline" size="sm" onClick={() => copy(vaCode, 'VA')} disabled={!vaCode}>Copy VA</Button>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={() => copy(billKey, 'Bill Key')} disabled={!billKey}>Copy Bill Key</Button>
|
<div className="mt-2 flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => copy(vaCode, 'VA')} disabled={!vaCode}>Copy VA</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => copy(billKey, 'Bill Key')} disabled={!billKey}>Copy Bill Key</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{selected && (
|
||||||
|
<div className="pt-2">
|
||||||
|
{selected === 'bca' ? (
|
||||||
|
<BcaInstructionList />
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20">
|
||||||
|
<div className="text-sm font-medium mb-2">Instruksi pembayaran</div>
|
||||||
|
<PaymentInstructions method="bank_transfer" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.</div>
|
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -259,7 +273,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{busy ? (
|
{busy ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2">
|
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 dark:border-black/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 dark:border-black/40 border-t-transparent" aria-hidden />
|
||||||
Membuat VA…
|
Membuat VA…
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type TabKey = 'mobile' | 'atm' | 'ib'
|
||||||
|
|
||||||
|
export function BcaInstructionList() {
|
||||||
|
const [tab, setTab] = React.useState<TabKey>('mobile')
|
||||||
|
return (
|
||||||
|
<div className="mt-2 rounded-lg border-2 border-black/20 dark:border-white/20 bg-white dark:bg-black/20">
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="text-sm font-medium mb-2">Instruksi BCA</div>
|
||||||
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Metode pembayaran BCA"
|
||||||
|
className="inline-flex mb-3 rounded-md border border-black/20 dark:border-white/20 overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="tab-bca-mobile"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tab === 'mobile'}
|
||||||
|
aria-controls="panel-bca-mobile"
|
||||||
|
onClick={() => setTab('mobile')}
|
||||||
|
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'mobile' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
BCA Mobile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="tab-bca-atm"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tab === 'atm'}
|
||||||
|
aria-controls="panel-bca-atm"
|
||||||
|
onClick={() => setTab('atm')}
|
||||||
|
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'atm' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
ATM BCA
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="tab-bca-ib"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tab === 'ib'}
|
||||||
|
aria-controls="panel-bca-ib"
|
||||||
|
onClick={() => setTab('ib')}
|
||||||
|
className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'ib' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
KlikBCA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{tab === 'mobile' && (
|
||||||
|
<div role="tabpanel" id="panel-bca-mobile" aria-labelledby="tab-bca-mobile">
|
||||||
|
<StepsMobile />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tab === 'atm' && (
|
||||||
|
<div role="tabpanel" id="panel-bca-atm" aria-labelledby="tab-bca-atm">
|
||||||
|
<StepsAtm />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tab === 'ib' && (
|
||||||
|
<div role="tabpanel" id="panel-bca-ib" aria-labelledby="tab-bca-ib">
|
||||||
|
<StepsKlikBca />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-black/10 dark:border-white/10 p-3 text-xs text-black/60 dark:text-white/60">
|
||||||
|
Catatan: Gunakan label "Nomor VA" secara konsisten di semua metode.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepsMobile() {
|
||||||
|
const steps = [
|
||||||
|
'Buka BCA Mobile, pilih m-BCA dan login.',
|
||||||
|
'Masuk ke menu m-Transfer.',
|
||||||
|
'Pilih BCA Virtual Account.',
|
||||||
|
'Masukkan Nomor VA dari halaman ini.',
|
||||||
|
'Periksa nama tujuan dan nominal, pastikan sesuai.',
|
||||||
|
'Kirim dan konfirmasi.',
|
||||||
|
'Masukkan PIN m-BCA untuk menyelesaikan pembayaran.'
|
||||||
|
]
|
||||||
|
return <StepList steps={steps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepsAtm() {
|
||||||
|
const steps = [
|
||||||
|
'Masukkan kartu dan PIN di ATM BCA.',
|
||||||
|
'Pilih Transaksi Lainnya.',
|
||||||
|
'Pilih Transfer.',
|
||||||
|
'Pilih ke BCA Virtual Account.',
|
||||||
|
'Masukkan Nomor VA dari halaman ini.',
|
||||||
|
'Periksa detail dan nominal, lalu konfirmasi.',
|
||||||
|
'Selesaikan transaksi dan simpan bukti.'
|
||||||
|
]
|
||||||
|
return <StepList steps={steps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepsKlikBca() {
|
||||||
|
const steps = [
|
||||||
|
'Login ke KlikBCA (Internet Banking).',
|
||||||
|
'Pilih menu Transfer Dana.',
|
||||||
|
'Pilih ke BCA Virtual Account.',
|
||||||
|
'Masukkan Nomor VA dari halaman ini.',
|
||||||
|
'Periksa detail dan nominal, lalu lanjutkan.',
|
||||||
|
'Masukkan respon KeyBCA untuk konfirmasi.',
|
||||||
|
'Transaksi selesai, simpan bukti/nota.'
|
||||||
|
]
|
||||||
|
return <StepList steps={steps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepList({ steps }: { steps: string[] }) {
|
||||||
|
return (
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-sm text-black/80 dark:text-white/80">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<li key={i}>{s}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -95,7 +95,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.</div>
|
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 space-y-2">
|
<div className="pt-2 space-y-2">
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-2 text-sm">
|
<div className="rounded border border-black/10 dark:border-white/10 p-2 text-sm" aria-live="polite">
|
||||||
<div className="font-medium">Kode Pembayaran</div>
|
<div className="font-medium">Kode Pembayaran</div>
|
||||||
{!selected && (
|
{!selected && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko terlebih dahulu di langkah sebelumnya.</div>
|
<div className="text-xs text-black/60 dark:text-white/60">Pilih toko terlebih dahulu di langkah sebelumnya.</div>
|
||||||
|
|
@ -109,10 +109,10 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
|
||||||
{selected && !busy && (storeFromRes || paymentCode) && (
|
{selected && !busy && (storeFromRes || paymentCode) && (
|
||||||
<>
|
<>
|
||||||
{storeFromRes ? <div>Toko: {storeFromRes.toUpperCase()}</div> : null}
|
{storeFromRes ? <div>Toko: {storeFromRes.toUpperCase()}</div> : null}
|
||||||
{paymentCode ? <div>Kode: <span className="font-mono">{paymentCode}</span></div> : null}
|
{paymentCode ? <div>Kode: <span className="font-mono text-lg tracking-[0.06em] select-all">{paymentCode}</span></div> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2"><Button variant="outline" size="sm" onClick={() => copy(paymentCode, 'Kode pembayaran')} disabled={!paymentCode || busy}>Copy Kode</Button></div>
|
<div className="mt-2"><Button variant="outline" className="w-full sm:w-auto" onClick={() => copy(paymentCode, 'Kode pembayaran')} disabled={!paymentCode || busy}>Copy Kode</Button></div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ function QRPlaceholder() {
|
||||||
|
|
||||||
export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) {
|
export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) {
|
||||||
const nav = usePaymentNavigation()
|
const nav = usePaymentNavigation()
|
||||||
const [showGuide, setShowGuide] = React.useState(false)
|
|
||||||
const [busy, setBusy] = React.useState(false)
|
const [busy, setBusy] = React.useState(false)
|
||||||
const [qrUrl, setQrUrl] = React.useState<string>('')
|
const [qrUrl, setQrUrl] = React.useState<string>('')
|
||||||
const [actions, setActions] = React.useState<Array<{ name?: string; method?: string; url: string }>>([])
|
const [actions, setActions] = React.useState<Array<{ name?: string; method?: string; url: string }>>([])
|
||||||
|
|
@ -77,38 +76,61 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
<GoPayLogosRow compact />
|
<GoPayLogosRow compact />
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-black/60 dark:text-white/60">Mode:</span>
|
<span className="text-black/60 dark:text-white/60">Mode:</span>
|
||||||
<div className="inline-flex rounded border border-black/10 dark:border-white/10 overflow-hidden">
|
<div className="inline-flex rounded-md border-2 border-black/20 dark:border-white/20 overflow-hidden" role="group" aria-label="Pilih mode pembayaran">
|
||||||
<button type="button" onClick={() => setMode('gopay')} className={`px-2 py-1 ${mode==='gopay' ? 'bg-black/80 text-white' : 'bg-white dark:bg-black text-black dark:text-white'}`}>GoPay</button>
|
<button
|
||||||
<button type="button" onClick={() => setMode('qris')} className={`px-2 py-1 ${mode==='qris' ? 'bg-black/80 text-white' : 'bg-white dark:bg-black text-black dark:text-white'}`}>QRIS</button>
|
type="button"
|
||||||
|
onClick={() => setMode('gopay')}
|
||||||
|
aria-pressed={mode==='gopay'}
|
||||||
|
className={`px-2 py-1 focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${mode==='gopay' ? 'bg-black text-white' : 'bg-white dark:bg-black text-black dark:text-white hover:bg-black/10 dark:hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
GoPay
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode('qris')}
|
||||||
|
aria-pressed={mode==='qris'}
|
||||||
|
className={`px-2 py-1 focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white dark:bg-black text-black dark:text-white hover:bg-black/10 dark:hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
QRIS
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded border border-black/10 dark:border-white/10 p-3 flex flex-col items-center gap-2">
|
<div className="rounded border border-black/10 dark:border-white/10 p-3 flex flex-col items-center gap-2">
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}</div>
|
<div className="text-xs text-black/60 dark:text-white/60">Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}</div>
|
||||||
<div className="relative w-full max-w-[240px] aspect-square grid place-items-center rounded-md border border-black/20 dark:border-white/20 bg-white">
|
<div className="relative w-full max-w-[280px] aspect-square grid place-items-center rounded-md border border-black/20 dark:border-white/20 bg-white">
|
||||||
{mode === 'qris' && (!qrUrl || busy) ? (
|
{mode === 'qris' && (!qrUrl || busy) ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2 text-xs text-black/60 dark:text-white/60" role="status" aria-live="polite">
|
<span className="inline-flex items-center justify-center gap-2 text-xs text-black/60 dark:text-white/60" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />
|
||||||
Membuat QR…
|
Membuat QR…
|
||||||
</span>
|
</span>
|
||||||
) : qrUrl ? (
|
) : qrUrl ? (
|
||||||
<img src={qrUrl} alt="QR untuk pembayaran" className="aspect-square w-full max-w-[220px] mx-auto" />
|
<img src={qrUrl} alt="QR untuk pembayaran" className="aspect-square w-full max-w-[260px] mx-auto" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-black/50 dark:text-white/50">Mode: {mode.toUpperCase()}</div>
|
<div className="text-[10px] text-black/50 dark:text-white/50">Mode: {mode.toUpperCase()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col sm:flex-row gap-2 w-full">
|
||||||
<Button variant="outline" size="sm" onClick={downloadQR} disabled={!qrUrl}>Download QR</Button>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={downloadQR} disabled={!qrUrl}>Download QR</Button>
|
||||||
<Button variant="outline" size="sm" onClick={openGoPay} disabled={mode === 'qris'}>Buka GoPay</Button>
|
<Button variant="outline" className="w-full sm:w-auto" onClick={openGoPay} disabled={mode === 'qris'}>Buka GoPay</Button>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2">
|
||||||
|
<PaymentInstructions
|
||||||
|
title={`Instruksi ${mode === 'gopay' ? 'GoPay' : 'QRIS'}`}
|
||||||
|
steps={mode === 'gopay'
|
||||||
|
? [
|
||||||
|
'Buka aplikasi GoPay dan pilih menu Scan.',
|
||||||
|
'Arahkan kamera ke QR di layar.',
|
||||||
|
'Periksa detail dan konfirmasi pembayaran di aplikasi.',
|
||||||
|
'Simpan bukti pembayaran; status akan diperbarui otomatis.'
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'Buka aplikasi e-wallet/Bank yang mendukung QRIS (GoPay, ShopeePay, dll).',
|
||||||
|
'Pilih menu Scan, arahkan kamera ke QR di layar.',
|
||||||
|
'Periksa detail dan konfirmasi pembayaran di aplikasi.',
|
||||||
|
'Simpan bukti pembayaran; status akan diperbarui otomatis.'
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowGuide((v) => !v)}
|
|
||||||
className="text-xs text-brand-600 hover:underline"
|
|
||||||
aria-expanded={showGuide}
|
|
||||||
>
|
|
||||||
Cara bayar
|
|
||||||
</button>
|
|
||||||
{showGuide && <PaymentInstructions method="gopay" />}
|
|
||||||
{locked && (
|
{locked && (
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.</div>
|
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -121,7 +143,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
|
||||||
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 ? (
|
{busy ? (
|
||||||
<span className="inline-flex items-center justify-center gap-2">
|
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
|
||||||
Menuju status…
|
Menuju status…
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { PaymentMethod } from './PaymentMethodList'
|
import type { PaymentMethod } from './PaymentMethodList'
|
||||||
|
|
||||||
export function PaymentInstructions({ method }: { method: PaymentMethod }) {
|
export function PaymentInstructions({ method, title, steps }: { method?: PaymentMethod; title?: string; steps?: string[] }) {
|
||||||
const steps = getSteps(method)
|
const computed = steps ?? (method ? getSteps(method) : [])
|
||||||
|
const finalSteps = computed.length ? computed : ['Ikuti instruksi yang muncul pada layar pembayaran.']
|
||||||
|
const finalTitle = title ?? 'Cara bayar'
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 rounded border border-black/10 dark:border-white/10 p-3 bg-black/2 dark:bg-white/5">
|
<div className="mt-2 rounded-lg border-2 border-black/30 dark:border-white/30 p-3 bg-white dark:bg-black/20" role="region" aria-label={finalTitle}>
|
||||||
<div className="text-sm font-medium mb-2">Cara bayar</div>
|
<div className="text-sm font-medium mb-2">{finalTitle}</div>
|
||||||
<ol className="list-decimal list-inside space-y-1 text-xs text-black/80 dark:text-white/80">
|
<ol className="list-decimal list-inside space-y-2 text-sm text-black/80 dark:text-white/80">
|
||||||
{steps.map((s, i) => (
|
{finalSteps.map((s, i) => (
|
||||||
<li key={i}>{s}</li>
|
<li key={i}>{s}</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
|
||||||
|
|
@ -30,26 +30,26 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium">Metode pembayaran</div>
|
<div className="text-sm font-medium">Metode pembayaran</div>
|
||||||
<div className="divide-y divide-black/10 dark:divide-white/10 rounded border border-black/10 dark:border-white/10">
|
<div className="rounded-lg border-2 border-black/30 dark:border-white/30 divide-y-[2px] divide-black/20 dark:divide-white/20 bg-white dark:bg-black">
|
||||||
{items.map((it) => (
|
{items.map((it) => (
|
||||||
<div key={it.key}>
|
<div key={it.key}>
|
||||||
<button
|
<button
|
||||||
onClick={() => !disabled && onSelect(it.key)}
|
onClick={() => !disabled && onSelect(it.key)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full text-left p-3 flex items-center justify-between ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-black/5 dark:hover:bg-white/10'} ${selected === it.key ? 'bg-black/5 dark:bg-white/10' : ''}`}
|
className={`w-full text-left p-4 min-h-[52px] flex items-center justify-between ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer hover:bg-black/10 dark:hover:bg-white/15'} ${selected === it.key ? 'bg-black/10 dark:bg-white/20' : ''} focus-visible:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-black`}
|
||||||
aria-pressed={selected === it.key}
|
aria-pressed={selected === it.key}
|
||||||
aria-expanded={selected === it.key}
|
aria-expanded={selected === it.key}
|
||||||
aria-controls={`panel-${it.key}`}
|
aria-controls={`panel-${it.key}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{it.title}</div>
|
<div className="text-base font-semibold text-black dark:text-white">{it.title}</div>
|
||||||
<div className="text-xs text-black/60 dark:text-white/60">{it.subtitle}</div>
|
<div className="text-sm text-black/70 dark:text-white/70">{it.subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="hidden sm:block" aria-hidden>
|
<span className="hidden sm:block" aria-hidden>
|
||||||
{it.icon}
|
{it.icon}
|
||||||
</span>
|
</span>
|
||||||
<span aria-hidden className={`text-black/40 dark:text-white/40 transition-transform ${selected === it.key ? 'rotate-90' : ''}`}>›</span>
|
<span aria-hidden className={`text-black/60 dark:text-white/60 text-lg transition-transform ${selected === it.key ? 'rotate-90' : ''}`}>›</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{selected === it.key && renderPanel && (
|
{selected === it.key && renderPanel && (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { TrustStrip } from './TrustStrip'
|
import { TrustStrip } from './TrustStrip'
|
||||||
|
|
||||||
function formatCurrencyIDR(amount: number) {
|
function formatCurrencyIDR(amount: number) {
|
||||||
|
|
@ -33,18 +34,27 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-[#0c1f3f] text-white p-4 flex items-center justify-between">
|
<div className="bg-[#0c1f3f] text-white p-3 sm:p-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<div className="rounded bg-white text-black px-2 py-1 text-xs font-bold">ZARA</div>
|
<div className="rounded bg-white text-black px-2 py-1 text-[11px] sm:text-xs font-bold" aria-hidden>
|
||||||
<div className="font-semibold">{merchantName}</div>
|
ZARA
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold text-sm sm:text-base">{merchantName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<div className="text-xs text-white/80">Bayar dalam <span className="font-semibold text-white">{countdown}</span></div>
|
<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}
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
className={`text-white/80 transition-transform ${expanded ? '' : 'rotate-180'}`}
|
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`}
|
||||||
>
|
>
|
||||||
˅
|
˅
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -65,6 +75,16 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
|
||||||
{children}
|
{children}
|
||||||
<TrustStrip location="sheet" />
|
<TrustStrip location="sheet" />
|
||||||
</div>
|
</div>
|
||||||
|
{/* Sticky CTA (mobile-friendly) */}
|
||||||
|
<div className="sticky bottom-0 bg-white/95 dark:bg-neutral-900/95 backdrop-blur border-t border-black/10 dark:border-white/10 p-3 pb-[env(safe-area-inset-bottom)]">
|
||||||
|
<Link
|
||||||
|
to={`/payments/${orderId}/status`}
|
||||||
|
aria-label="Buka halaman status pembayaran"
|
||||||
|
className="w-full block text-center rounded bg-[#0c1f3f] text-white py-3 text-base font-semibold hover:bg-[#0a1a35] focus:outline-none focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-[#0c1f3f]"
|
||||||
|
>
|
||||||
|
Cek status pembayaran
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue