diff --git a/docs/front-end-spec.md b/docs/front-end-spec.md new file mode 100644 index 0000000..d857c6a --- /dev/null +++ b/docs/front-end-spec.md @@ -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. \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e92b1af..0a8eb36 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '../../lib/cn' 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: { variant: { diff --git a/src/features/payments/components/BankTransferPanel.tsx b/src/features/payments/components/BankTransferPanel.tsx index ab16556..457a5eb 100644 --- a/src/features/payments/components/BankTransferPanel.tsx +++ b/src/features/payments/components/BankTransferPanel.tsx @@ -2,6 +2,7 @@ import { Button } from '../../../components/ui/button' import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from './PaymentInstructions' +import { BcaInstructionList } from './BcaInstructionList' import { TrustStrip } from './TrustStrip' import { BankLogosRow, BankLogo, type BankKey } from './PaymentLogos' import { postCharge } from '../../../services/api' @@ -126,6 +127,13 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, // eslint-disable-next-line react-hooks/exhaustive-deps }, [selected]) + // Auto-show instructions when BCA is selected to reduce confusion + React.useEffect(() => { + if (selected === 'bca' && !showGuide) { + setShowGuide(true) + } + }, [selected]) + return (
Transfer bank
@@ -137,43 +145,49 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, {selected.toUpperCase()}
)} -
VA dibuat otomatis sesuai bank pilihan Anda.
+
VA dibuat otomatis sesuai bank pilihan Anda.
{errorMessage && ( {errorMessage} )} - - {showGuide && } {selected && ( -
-
- {vaCode ? ( - VA: {vaCode} - ) : ( - - {busy && } - {busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'} - - )} - {billKey && ( - Bill Key: {billKey} - )} - {billerCode && ( - Biller Code: {billerCode} - )} -
-
- - +
+
+
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} + )} +
+
+ + +
)} + {selected && ( +
+ {selected === 'bca' ? ( + + ) : ( +
+
Instruksi pembayaran
+ +
+ )} +
+ )} {locked && (
Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.
)} @@ -259,7 +273,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, className="w-full" > {busy ? ( - + Membuat VA… diff --git a/src/features/payments/components/BcaInstructionList.tsx b/src/features/payments/components/BcaInstructionList.tsx new file mode 100644 index 0000000..ea969de --- /dev/null +++ b/src/features/payments/components/BcaInstructionList.tsx @@ -0,0 +1,120 @@ +import React from 'react' + +type TabKey = 'mobile' | 'atm' | 'ib' + +export function BcaInstructionList() { + const [tab, setTab] = React.useState('mobile') + return ( +
+
+
Instruksi BCA
+
+ + + +
+ {tab === 'mobile' && ( +
+ +
+ )} + {tab === 'atm' && ( +
+ +
+ )} + {tab === 'ib' && ( +
+ +
+ )} +
+
+ Catatan: Gunakan label "Nomor VA" secara konsisten di semua metode. +
+
+ ) +} + +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 +} + +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 +} + +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 +} + +function StepList({ steps }: { steps: string[] }) { + return ( +
    + {steps.map((s, i) => ( +
  1. {s}
  2. + ))} +
+ ) +} \ No newline at end of file diff --git a/src/features/payments/components/CStorePanel.tsx b/src/features/payments/components/CStorePanel.tsx index cac1e82..ae8cb49 100644 --- a/src/features/payments/components/CStorePanel.tsx +++ b/src/features/payments/components/CStorePanel.tsx @@ -95,7 +95,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
Metode terkunci. Gunakan kode pembayaran di kasir {selected?.toUpperCase()}.
)}
-
+
Kode Pembayaran
{!selected && (
Pilih toko terlebih dahulu di langkah sebelumnya.
@@ -109,10 +109,10 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul {selected && !busy && (storeFromRes || paymentCode) && ( <> {storeFromRes ?
Toko: {storeFromRes.toUpperCase()}
: null} - {paymentCode ?
Kode: {paymentCode}
: null} + {paymentCode ?
Kode: {paymentCode}
: null} )} -
+
- +
+ +
Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}
-
+
{mode === 'qris' && (!qrUrl || busy) ? ( Membuat QR… ) : qrUrl ? ( - QR untuk pembayaran + QR untuk pembayaran ) : null}
Mode: {mode.toUpperCase()}
-
- - +
+ + +
+
+
- - {showGuide && } {locked && (
Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.
)} @@ -121,7 +143,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord onClick={() => { setBusy(true); onChargeInitiated?.(); setTimeout(() => { nav.toStatus(orderId, mode) ; setBusy(false) }, 250) }} > {busy ? ( - + Menuju status… diff --git a/src/features/payments/components/PaymentInstructions.tsx b/src/features/payments/components/PaymentInstructions.tsx index cf91fbc..cb432c7 100644 --- a/src/features/payments/components/PaymentInstructions.tsx +++ b/src/features/payments/components/PaymentInstructions.tsx @@ -1,13 +1,15 @@ import React from 'react' import type { PaymentMethod } from './PaymentMethodList' -export function PaymentInstructions({ method }: { method: PaymentMethod }) { - const steps = getSteps(method) +export function PaymentInstructions({ method, title, steps }: { method?: PaymentMethod; title?: string; steps?: string[] }) { + const computed = steps ?? (method ? getSteps(method) : []) + const finalSteps = computed.length ? computed : ['Ikuti instruksi yang muncul pada layar pembayaran.'] + const finalTitle = title ?? 'Cara bayar' return ( -
-
Cara bayar
-
    - {steps.map((s, i) => ( +
    +
    {finalTitle}
    +
      + {finalSteps.map((s, i) => (
    1. {s}
    2. ))}
    diff --git a/src/features/payments/components/PaymentMethodList.tsx b/src/features/payments/components/PaymentMethodList.tsx index 15742cb..2f293aa 100644 --- a/src/features/payments/components/PaymentMethodList.tsx +++ b/src/features/payments/components/PaymentMethodList.tsx @@ -30,26 +30,26 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e return (
    Metode pembayaran
    -
    +
    {items.map((it) => (
    {selected === it.key && renderPanel && ( diff --git a/src/features/payments/components/PaymentSheet.tsx b/src/features/payments/components/PaymentSheet.tsx index e5cd44b..fbcbb09 100644 --- a/src/features/payments/components/PaymentSheet.tsx +++ b/src/features/payments/components/PaymentSheet.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { Link } from 'react-router-dom' import { TrustStrip } from './TrustStrip' function formatCurrencyIDR(amount: number) { @@ -33,18 +34,27 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
    {/* Header */} -
    -
    -
    ZARA
    -
    {merchantName}
    +
    +
    +
    + ZARA +
    +
    {merchantName}
    -
    -
    Bayar dalam {countdown}
    +
    +
    + Kedaluwarsa dalam {countdown} +
    @@ -65,6 +75,16 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, {children}
    + {/* Sticky CTA (mobile-friendly) */} +
    + + Cek status pembayaran + +
    )