41 KiB
Midtrans CIFO Frontend Architecture Document
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2025-11-07 | v1 | Initial frontend architecture scaffolding | Architect (Winston) |
1. Template and Framework Selection
Context & Decision
- Basis dokumen:
docs/prd.md(UI multi-metode pembayaran: VA, QRIS, Kartu) dan belum ada dokumen arsitektur utama (docs/architecture.md). - Lingkungan proyek: Laragon (PHP) untuk backend; frontend akan diatur sebagai aplikasi SPA terpisah yang berkomunikasi via REST.
- Starter pilihan: Vite + React + TypeScript.
Rationale
- Vite + React memberikan setup cepat, build cepat, dan ekosistem luas. TypeScript membantu kualitas kode meski QA dilakukan manual (type-safety, DX lebih baik).
- React Router memadai untuk halaman Checkout, Status, dan Riwayat; TanStack Query menangani data fetching, status, cache, dan retry/polling untuk status transaksi.
- Tailwind CSS mempercepat UI dengan gaya konsisten, aksesibilitas lebih terukur, dan footprint tooling rendah.
- Tidak memilih Next.js karena kebutuhan SSR tidak kritikal untuk halaman checkout sederhana, dan menghindari kompleksitas deploy.
Konsekuensi
- Manual QA only: tidak ada pipeline test otomatis; struktur proyek dan standar coding harus tegas untuk menghindari regressi.
- Build/deploy dilakukan manual; dokumentasi command harus jelas (dev/build). Observability di UI via logging ringan & event tracking internal.
2. Frontend Tech Stack (Draft untuk Validasi)
| Category | Technology | Version | Purpose | Rationale |
|---|---|---|---|---|
| Framework | React | ^18 | SPA untuk checkout dan status transaksi | Ekosistem luas, composable UI, dukungan community yang kuat |
| UI Library | Tailwind CSS | ^3 | Styling utilitas cepat | Cepat, konsisten, mudah diadopsi, cocok untuk MVP |
| State Management | TanStack Query | ^5 | Server state (fetch/poll/retry/cache) | Ideal untuk status transaksi & polling; minimal boilerplate |
| Routing | React Router | ^6 | Navigasi halaman | Pola rute sederhana, proteksi rute mudah |
| Build Tool | Vite | ^5 | Dev/build tooling | Startup cepat, konfigurasi sederhana |
| Styling | Tailwind + CSS Modules | ^3 | Styling kombinasi utilitas + modular | Fleksibel; module untuk komponen kompleks |
| Testing | Manual QA | N/A | Pengujian manual | Sesuai kebijakan proyek (tanpa CI/CD); template test dapat ditambahkan |
| Component Library | Headless UI / Shadcn | latest | Komponen aksesibel minimal | Mempercepat pembuatan form/overlay; tetap fleksibel |
| Form Handling | React Hook Form | ^7 | Validasi & kontrol form | Ringan, ergonomis, integrasi baik dengan TS |
| Animation | Framer Motion | ^11 | Mikro-animasi & transisi | Peningkatan UX tanpa kompleksitas berlebih |
| Dev Tools | ESLint + Prettier | latest | Konsistensi & kualitas kode | Membantu kualitas saat QA manual; cegah kesalahan umum |
Rationale Tambahan
- TanStack Query mempermudah implementasi polling status transaksi (fallback ketika webhook belum tiba) dan retry terukur sesuai PRD.
- React Hook Form memudahkan validasi input kartu (tokenisasi) dan interaksi form minimal.
- Headless UI/Shadcn memberi komponen aksesibel untuk dialog/menus tanpa mengikat ke desain tertentu.
Elicit Options (1–9)
- Proceed ke bagian berikutnya (Project Structure)
- Risk & Challenge Analysis untuk pilihan stack
- Structural Analysis (komponen vs halaman vs services)
- Devil’s Advocate (tantangan stack ini untuk manual QA)
- Multi-Persona (PO/SM/Dev/QA) feedback cepat
- Creative Exploration (alternatif: Vue/Nuxt atau Angular)
- 2025 Techniques (SSR-lite/Partial Hydration pertimbangan)
- Process Control (tandai asumsi dan batasan eksplisit)
- Reflective Review (cek keselarasan dengan PRD Epics 1–3)
Select 1–9 or just type your question/feedback:
7. Routing
Tujuan: mendefinisikan peta rute, alur navigasi Checkout → Status → Riwayat, serta praktik React Router v6 (lazy loading, error boundaries, guard ringan).
7.1 Route Map
/checkout— halaman utama untuk memilih metode (VA, QRIS/GoPay, Card, Cstore) dan memulai charge./payments/:orderId/status— halaman status transaksi real‑time (polling/sse) untuk semua metode./history— riwayat transaksi (read-only, filter/sort sederhana).- Opsional (Card 3DS):
/checkout/card/challenge— halaman penanganan challenge bila diperlukan; atau gunakanredirect_urleksternal. - Fallback:
*— NotFound.
7.2 Konfigurasi React Router (v6)
// src/app/router.tsx
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import { AppLayout } from './AppLayout';
import { CheckoutPage } from '../pages/CheckoutPage';
import { PaymentStatusPage } from '../pages/PaymentStatusPage';
import { PaymentHistoryPage } from '../pages/PaymentHistoryPage';
import { NotFoundPage } from '../pages/NotFoundPage';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <div role="alert">Terjadi kesalahan. Coba muat ulang.</div>,
children: [
{ index: true, element: <Navigate to="/checkout" replace /> },
{ path: 'checkout', element: <CheckoutPage /> },
{ path: 'payments/:orderId/status', element: <PaymentStatusPage /> },
{ path: 'history', element: <PaymentHistoryPage /> },
{ path: '*', element: <NotFoundPage /> },
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
Catatan:
- Gunakan lazy loading untuk halaman berat (mis.
PaymentHistoryPage) guna mempercepat initial load checkout. - Tambahkan
<ScrollRestoration />di layout bila perlu.
7.3 Alur Navigasi
- Dari
/checkout, setelahchargesukses (menerimaorder_id), arahkan ke/payments/:orderId/status. - Simpan
payment_typedilocation.stateatau query (contoh:?type=gopay) bila diperlukan untuk UI hint. - Terminal state:
settlement: tampilkan success dan CTA kembali ke/history.expire/cancel/deny: tampilkan informasi dan CTA ulang di/checkout.
- Card (3DS): bila response berisi
redirect_url, buka di jendela/tab baru atau navigasi ke/checkout/card/challengeyang menangani proses lalu kembali ke/payments/:orderId/status.
Contoh helper navigasi:
// src/features/payments/lib/navigation.ts
import { useNavigate } from 'react-router-dom';
export function usePaymentNavigation() {
const navigate = useNavigate();
return {
toStatus(orderId: string, paymentType?: string) {
navigate(`/payments/${orderId}/status`, {
state: paymentType ? { paymentType } : undefined,
replace: true,
});
},
toHistory() {
navigate('/history');
},
toCheckout() {
navigate('/checkout');
},
};
}
7.4 Guards & Recovery
- Validasi akses langsung ke
/payments/:orderId/status:- Query
getPaymentStatus(orderId)— jika 404/invalid, tampilkan NotFound atau ajak kembali ke/checkout.
- Query
- Lindungi
/historydengan batasan sederhana (misal hanya read-only; tidak memerlukan auth jika PRD tidak mensyaratkan). - Toleransi refresh: halaman status dapat dibuka ulang langsung selama
order_idvalid.
7.5 Status Updates (Polling / Push)
- Default: polling 3 detik menggunakan TanStack Query seperti di bagian State Management.
- Opsi 2025: SSE/WebSocket untuk push status agar real‑time dan hemat jaringan; fallback ke polling saat koneksi tidak stabil.
7.6 Aksesibilitas & Analytics
- Announce perubahan status dengan ARIA live region (mis.
role="status",aria-live="polite"). - Catat event navigasi (route change) beserta
X-Correlation-Iduntuk tracing ringan di UI.
Elicit Options (Routing → Next)
- Lanjut ke Styling (Tailwind arsitektur dan utility patterns)
- Tambah Testing (rencana manual QA dan template kasus uji)
- Environment Config (env vars & build/dev commands)
- Developer Standards (lint, format, commit hygiene)
- Error UX polish (banner, retry, deep link handling)
- Performance (lazy routes, code-splitting, prefetch)
8. Styling
Tujuan: menetapkan arsitektur styling yang konsisten dan dapat dipelihara menggunakan Tailwind CSS (utilitas) dan CSS Modules (gaya terisolasi), termasuk varian komponen, responsif, dark mode, dan a11y visual.
8.1 Setup Tailwind (config + globals)
Konfigurasi dasar Tailwind dengan dark mode berbasis class dan plugin formulir/typografi.
// tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626', // merah utama
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
},
boxShadow: {
focus: '0 0 0 3px rgba(220,38,38,0.45)',
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
} satisfies Config;
Global stylesheet minimal: reset, font, dan token CSS (opsional) untuk warna.
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--radius: 8px;
}
@layer base {
html, body, #root { height: 100%; }
body { @apply antialiased bg-white text-black; }
.dark body { @apply bg-black text-white; }
a { @apply text-brand-600 hover:text-brand-700; }
}
@layer utilities {
.focus-ring { @apply outline-none ring-2 ring-brand-600 ring-offset-2 ring-offset-white dark:ring-offset-black; }
.card { @apply rounded-[var(--radius)] border border-black/10 bg-white shadow-sm dark:bg-black dark:border-white/20; }
}
8.2 Pola Utilitas & Komposisi
- Gunakan utilitas untuk layout:
flex,grid,gap,container,max-w-*,space-*. - Terapkan skala spacing/typography konsisten:
text-sm/base/lg,p-2/3/4,gap-2/3/4. - Komposisi class dinamis via
clsx/cnhelper.
// src/lib/cn.ts
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: Array<string | Record<string, boolean> | undefined>) {
return twMerge(clsx(inputs));
}
Contoh penggunaan:
// src/components/Section.tsx
import { cn } from '../lib/cn';
export function Section(props: { title: string; className?: string; children: React.ReactNode }) {
return (
<section className={cn('mb-6', props.className)}>
<h2 className="text-lg font-semibold mb-2">{props.title}</h2>
{props.children}
</section>
);
}
8.3 Varian Komponen dengan CVA (opsional)
Gunakan class-variance-authority untuk varian yang dapat dikontrol tanpa membengkakkan class string.
// src/components/ui/button.tsx
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',
{
variants: {
variant: {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-white text-black hover:bg-white/90 dark:bg-black dark:text-white dark:hover:bg-black/90',
outline: 'border border-black text-black hover:bg-black/5 dark:border-white dark:text-white dark:hover:bg-white/10',
},
size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-6' },
},
defaultVariants: { variant: 'primary', size: 'md' },
}
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
8.4 CSS Modules untuk Gaya Terkapsulasi
Gunakan CSS Modules untuk komponen kompleks yang membutuhkan selector stateful atau animasi khusus sambil memanfaatkan @apply Tailwind.
/* src/components/alert/Alert.module.css */
.root { @apply card p-4 flex gap-3; }
.icon { @apply text-brand-600; }
.title { @apply font-semibold; }
.desc { @apply text-sm text-slate-600 dark:text-slate-300; }
// src/components/alert/Alert.tsx
import s from './Alert.module.css';
export function Alert({ title, children }: { title: string; children?: React.ReactNode }) {
return (
<div className={s.root} role="status" aria-live="polite">
<div className={s.icon}>⚑</div>
<div>
<div className={s.title}>{title}</div>
{children ? <div className={s.desc}>{children}</div> : null}
</div>
</div>
);
}
8.5 Responsif & Dark Mode
- Breakpoint standar:
sm,md,lg,xl,2xl; gunakan progressif (mobile-first) dan hindari override berlebihan. - Dark mode via class
darkdi taghtml/body; toggling dilakukan oleh provider tema UI (lihat State Management bagian UI-store). - Gambar/ikon: pilih versi gelap/terang atau gunakan
opacity/mix-blenduntuk adaptasi.
8.6 Aksesibilitas Visual
- Fokus terlihat: gunakan
.focus-ringdanfocus-visibleagar keyboard navigation jelas. - Kontras warna memenuhi WCAG AA; warna brand 600+ untuk tombol utama, teks tetap terbaca di dark mode.
- ARIA untuk komponen interaktif (dialog, toast, status) sesuai rekomendasi Headless UI/Shadcn.
8.7 Performansi & Maintainability
- Hindari class string sangat panjang—ekstrak ke komponen kecil atau helper varian.
- Gunakan
prettier-plugin-tailwindcssuntuk pengurutan class otomatis (opsional, non-blocking). - Pastikan purge/content paths tepat agar ukuran CSS hasil build tetap kecil.
Elicit Options (Styling → Next)
- Testing (kasus uji manual dan template skenario)
- Environment Config (env vars, build/dev command)
- Developer Standards (lint/format/commit hygiene)
- Error UX polish (banner, retry, deep link)
- Performance (code-splitting, prefetch assets)
- Theming tokens (CSS vars dan skala desain)
6. API Integration
Tujuan: mendefinisikan batas frontend–backend, kontrak endpoint REST, tipe respons yang relevan untuk UI, dan layanan frontend untuk VA (Bank Transfer), Card (3DS), E-Wallet/QRIS (GoPay), serta Over the Counter (cstore). Sesuai kebijakan keamanan, semua charge dilakukan di backend (Server Key).
6.1 Backend Endpoints (Kontrak)
POST /api/payments/charge- Body (umum):
payment_type:'bank_transfer' | 'permata' | 'echannel' | 'credit_card' | 'gopay' | 'cstore'transaction_details:{ gross_amount: number, order_id?: string }- Parameter metode:
- Bank Transfer (VA):
{ bank_transfer: { bank: 'bca' | 'bni' | 'bri' | 'cimb' } } - Permata VA:
payment_type: 'permata'(tanpabank_transferbody) - Mandiri Bill Payment (E-Channel):
payment_type: 'echannel',echannel: { bill_info1?: string, bill_info2?: string } - Card:
{ credit_card: { token_id: string, authentication: true } }(token diambil di frontend via 3DS script) - E-Wallet/QRIS:
{ gopay: { enable_callback?: boolean, callback_url?: string } } - Cstore:
{ cstore: { store: 'alfamart' | 'indomaret', message?: string, ... } }
- Bank Transfer (VA):
- Response (disederhanakan untuk UI):
- Bank Transfer:
{ order_id, payment_type:'bank_transfer', bank, va_number, status:'pending', expiry_time }(backend menormalkanva_numbersmenjadi satuva_number) - Permata VA:
{ order_id, payment_type:'permata', va_number, status:'pending', expiry_time }(backend menormalkanpermata_va_number→va_number) - Mandiri E-Channel:
{ order_id, payment_type:'echannel', bill_key, biller_code, status:'pending', expiry_time } - Card:
{ order_id, payment_type:'credit_card', status:'pending' | 'challenge' | 'capture' | 'settlement', redirect_url? } - QRIS/GoPay:
{ order_id, payment_type:'gopay', qr_url, deeplink_url?, expiry_time, status:'pending' } - Cstore:
{ order_id, payment_type:'cstore', payment_code, store, status:'pending', expiry_time }
- Bank Transfer:
- Body (umum):
GET /api/payments/:orderId/status- Response:
{ order_id, payment_type, status: 'pending' | 'settlement' | 'deny' | 'cancel' | 'expire', transaction_time?, gross_amount?, fraud_status? }
- Response:
POST /api/payments/:orderId/cancel(opsional; sebelum settlement)POST /api/payments/:orderId/refund(opsional; admin flow)POST /api/payments/webhook(backend only) — UI tidak memanggil; status dipush ke DB, lalu UI membaca via endpointstatus.
Catatan keamanan:
- Server Key hanya di backend; frontend memakai Client Key untuk 3DS script.
- Backend memvalidasi
order_iduntuk idempotensi; frontend boleh mengirim atau backend dapat membuatkan. - Header standar:
X-Correlation-Iduntuk tracing.
6.2 Frontend Services (contracts)
services/apiClient.ts — klien HTTP berbasis Axios (atau fetch) dengan base URL dari VITE_API_BASE_URL.
// src/features/payments/services/apiClient.ts
import axios from 'axios';
const apiBase = import.meta.env.VITE_API_BASE_URL;
export const apiClient = axios.create({ baseURL: apiBase });
apiClient.interceptors.request.use((config) => {
config.headers = {
...config.headers,
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Correlation-Id': crypto?.randomUUID?.() ?? String(Date.now()),
};
return config;
});
services/paymentsService.ts — fungsi-fungsi terpisah per metode.
// src/features/payments/services/paymentsService.ts
import { apiClient } from './apiClient';
export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore';
export type PaymentStatus = 'pending' | 'challenge' | 'capture' | 'settlement' | 'deny' | 'cancel' | 'expire';
export async function chargeVA(bank: 'bca' | 'bni' | 'bri' | 'cimb') {
const res = await apiClient.post('/payments/charge', {
payment_type: 'bank_transfer',
bank_transfer: { bank },
});
return res.data as {
order_id: string;
payment_type: 'bank_transfer';
bank: string;
va_number: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargePermataVA() {
const res = await apiClient.post('/payments/charge', {
payment_type: 'permata',
});
return res.data as {
order_id: string;
payment_type: 'permata';
va_number: string; // normalized from permata_va_number
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeMandiriEChannel(billInfo?: { bill_info1?: string; bill_info2?: string }) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'echannel',
echannel: billInfo ?? {},
});
return res.data as {
order_id: string;
payment_type: 'echannel';
bill_key: string;
biller_code: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeCard(tokenId: string, authentication = true) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'credit_card',
credit_card: { token_id: tokenId, authentication },
});
return res.data as {
order_id: string;
payment_type: 'credit_card';
status: PaymentStatus;
redirect_url?: string;
};
}
export async function chargeQRIS(options?: { enable_callback?: boolean; callback_url?: string }) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'gopay',
gopay: options ?? {},
});
return res.data as {
order_id: string;
payment_type: 'gopay';
qr_url: string;
deeplink_url?: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function chargeCstore(store: 'alfamart' | 'indomaret', message?: string) {
const res = await apiClient.post('/payments/charge', {
payment_type: 'cstore',
cstore: { store, message },
});
return res.data as {
order_id: string;
payment_type: 'cstore';
payment_code: string;
store: string;
status: PaymentStatus;
expiry_time?: string;
};
}
export async function getPaymentStatus(orderId: string) {
const res = await apiClient.get(`/payments/${orderId}/status`);
return res.data as {
order_id: string;
payment_type: PaymentMethod;
status: PaymentStatus;
transaction_time?: string;
gross_amount?: number;
fraud_status?: string;
};
}
6.3 Env Vars & Script Loader (Card 3DS)
- Frontend
.env.example:VITE_API_BASE_URL=https://localhost:8000/apiVITE_MIDTRANS_CLIENT_KEY=YOUR_CLIENT_KEYVITE_MIDTRANS_ENV=sandbox(atauproduction)
- Komponen loader script 3DS:
// src/components/ScriptLoader.tsx
import { useEffect } from 'react';
export function Midtrans3DSLoader() {
useEffect(() => {
const script = document.createElement('script');
script.id = 'midtrans-script';
script.type = 'text/javascript';
script.src = 'https://api.midtrans.com/v2/assets/js/midtrans-new-3ds.min.js';
script.setAttribute('data-environment', import.meta.env.VITE_MIDTRANS_ENV);
script.setAttribute('data-client-key', import.meta.env.VITE_MIDTRANS_CLIENT_KEY);
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
return null;
}
Penggunaan: Render Midtrans3DSLoader di CheckoutPage atau App saat flow kartu diperlukan.
6.4 Error Contract & Mapping
- Standar error dari backend:
- 400:
VALIDATION_ERROR(contoh: kartu gagal Luhn, parameter salah) - 401/403:
AUTH_ERROR - 409:
ORDER_CONFLICT(duplikasiorder_id) - 5xx:
SERVER_ERROR
- 400:
- UI menampilkan pesan ramah di
ErrorBannerdan menyediakan CTARetry,Back,Contact Supportsesuai konteks. - Logging ringan di
lib/logger.tsmencatatX-Correlation-Iduntuk pelacakan.
6.5 Alur per Metode (Ringkas)
- VA:
chargeVA(bank)→ tampilkanva_number+ instruksi; arahkan kePaymentStatus; polling hinggasettlement/expire.
- Card (3DS):
- Muat 3DS script →
MidtransNew3ds.getCardToken(...)→ kirimtoken_idke backend viachargeCard(token)→ arahkan kePaymentStatus; status bisachallengesebelumsettlement.
- Muat 3DS script →
- QRIS/GoPay:
chargeQRIS()→ tampilkanqr_url(atau deeplink untuk mobile) + countdown; arahkan kePaymentStatus; polling status.
- Cstore:
chargeCstore(store)→ tampilkanpayment_code+ instruksi; arahkan kePaymentStatus; tunggu konfirmasi.
6.6 Boundary & Keamanan
- Tidak ada Server Key di frontend; semua request ke Midtrans dari backend.
- Client Key hanya untuk 3DS tokenization; token_id berlaku satu transaksi.
- Hindari menampilkan data sensitif; UI menyimpan minimal (status & indikator UX).
6.7 Schema Compatibility Notes (Midtrans Core API)
Tujuan: memastikan kontrak UI selaras dengan skema resmi Midtrans dan menjelaskan pemetaan/normalisasi yang dilakukan backend agar UI tetap sederhana.
-
Bank Transfer (VA)
- Request resmi:
payment_type: "bank_transfer",transaction_details.{order_id,gross_amount},bank_transfer.{bank}untuk BCA/BNI/BRI/CIMB [0]. - Response variasi:
va_numbers: [{ bank, va_number }](multi-bank), disederhanakan backend menjadi satuva_numberuntuk UI. - Permata: gunakan
payment_type: "permata"(bukanbank_transfer); response fieldpermata_va_numberdinormalisasi menjadiva_number[0]. - Mandiri Bill Payment (E-Channel): gunakan
payment_type: "echannel"denganechannel.{bill_info1,bill_info2}; response berisibill_keydanbiller_code[0].
- Request resmi:
-
Card (3DS)
- Frontend mengambil
token_idviamidtrans-new-3dsscript (Client Key) [1]. - Backend Charge:
payment_type: "credit_card",credit_card.{ token_id, authentication }[1]. - Response dapat mengandung
redirect_url(untuk challenge) dantransaction_statussepertipending,challenge,capture/settlement; UI menambahkan dukungancapture[1].
- Frontend mengambil
-
E-Wallet/QRIS (GoPay)
- Request:
payment_type: "gopay"; optionalgopay.{ enable_callback, callback_url }[2]. - Response:
actions(QR URL untuk desktop, deeplink untuk mobile) dantransaction_status: pending; backend menormalkan keqr_urldandeeplink_urluntuk konsumsi UI [2].
- Request:
-
Over the Counter (cstore)
- Request:
payment_type: "cstore",cstore.{ store, message, alfamart_free_text_1..3? }[3]. - Response:
payment_code,store,transaction_status: pending[3].
- Request:
-
Field umum yang dapat di-pass-through untuk debugging:
status_code,status_message,transaction_id,order_id,gross_amount,payment_type,transaction_time,fraud_status.
Catatan implementasi:
- Backend bertindak sebagai adapter yang menormalisasi variasi field antar metode/bank agar UI konsumsi konsisten.
- Jika diperlukan strictness penuh, UI dapat memakai nama field resmi (
transaction_status,va_numbers,actions) dan merender sesuai variasi.
Elicit Options (1–9)
- Proceed ke Routing (navigasi Checkout → Status → Riwayat)
- Risk & Challenge (kontrak API dan dampak QA manual)
- Structural Analysis (abstraksi services dan tipe respons)
- Devil’s Advocate (apakah satu endpoint
chargeterlalu generik?) - Multi-Persona (PO/SM/Dev/QA sudut pandang)
- Creative Exploration (layer adapter untuk future payment methods)
- 2025 Techniques (SSE/WebSocket untuk status push)
- Process Control (idempotensi & correlation id praktik)
- Reflective Review (keselarasan dengan PRD epics & Midtrans docs)
Select 1–9 or just type your question/feedback:
5. State Management
Tujuan: memisahkan dengan jelas server-state (status transaksi, hasil charge) dan UI-state (pilihan metode, dialog), menerapkan polling yang aman, dan konsisten dengan PRD (webhook + fallback polling).
5.1 Server-State dengan TanStack Query
- Query Client Defaults (di
providers/QueryProvider.tsx):refetchOnWindowFocus: false(hindari refetch tak terduga saat QA manual).retry(failCount, error): maksimal 3 kali untuk error jaringan/transient; mapping error 4xx jadi non-retry.staleTime: 3–5 detik untuk status transaksi; 0 untuk operasi lain.
- Query Keys Standar:
['payments','status', orderId]untuk status transaksi.['payments','details', orderId]untuk detail transaksi.- Mutations:
['payments','charge', method]untuk pembuatan transaksi.
- Invalidation & Lifecycle:
- Setelah
chargesukses, arahkan kePaymentStatusdan mulai pollingstatus. - Hentikan polling saat mencapai terminal state (
settlement,deny,cancel,expire). - Invalidate status jika webhook backend mengubah status (opsional: gunakan indikator versi/timestamp dari backend).
- Setelah
Contoh hook status dengan polling kondisional:
// features/payments/hooks/usePaymentStatus.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../services/apiClient';
const TERMINAL = new Set(['settlement', 'deny', 'cancel', 'expire']);
export function usePaymentStatus(orderId: string) {
return useQuery({
queryKey: ['payments', 'status', orderId],
queryFn: async () => {
const res = await apiClient.get(`/payments/${orderId}/status`);
return res.data; // { status: 'pending' | 'settlement' | ... }
},
refetchInterval: (data) => {
const status = data?.status;
return status && TERMINAL.has(status) ? false : 3000; // 3s polling
},
retry: (failCount, error: any) => {
const is4xx = error?.response?.status && String(error.response.status).startsWith('4');
if (is4xx) return false; // user error, jangan retry
return failCount < 3;
},
refetchOnWindowFocus: false,
});
}
Catatan:
- Status mengikuti pola Midtrans (contoh umum:
pending,settlement,deny,cancel,expire). - Untuk kartu, status dapat melalui tahapan otentikasi (3DS) sebelum menjadi
settlement.
5.2 UI-State dengan Store Tipis
Ruang lingkup UI-state: pilihan metode, visibilitas modal/dialog, notifikasi/UI flags (misal QR countdown). Simpan terpisah dari server-state.
Contoh store sederhana berbasis Context + Reducer:
// features/payments/store/uiStore.ts
import React, { createContext, useContext, useReducer } from 'react';
type PaymentMethod = 'VA' | 'QRIS' | 'Card' | 'Cstore' | null;
type UIState = {
selectedMethod: PaymentMethod;
isModalOpen: boolean;
toast: { type: 'info' | 'error' | 'success'; message: string } | null;
};
type Action =
| { type: 'SET_METHOD'; method: PaymentMethod }
| { type: 'OPEN_MODAL' }
| { type: 'CLOSE_MODAL' }
| { type: 'SHOW_TOAST'; toast: UIState['toast'] }
| { type: 'CLEAR_TOAST' };
const initialState: UIState = {
selectedMethod: null,
isModalOpen: false,
toast: null,
};
function reducer(state: UIState, action: Action): UIState {
switch (action.type) {
case 'SET_METHOD':
return { ...state, selectedMethod: action.method };
case 'OPEN_MODAL':
return { ...state, isModalOpen: true };
case 'CLOSE_MODAL':
return { ...state, isModalOpen: false };
case 'SHOW_TOAST':
return { ...state, toast: action.toast };
case 'CLEAR_TOAST':
return { ...state, toast: null };
default:
return state;
}
}
const UIContext = createContext<{ state: UIState; dispatch: React.Dispatch<Action> } | undefined>(
undefined,
);
export function UIProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return <UIContext.Provider value={{ state, dispatch }}>{children}</UIContext.Provider>;
}
export function useUI() {
const ctx = useContext(UIContext);
if (!ctx) throw new Error('useUI must be used within UIProvider');
return ctx;
}
Prinsip:
- UI-state tidak menyimpan data transaksi dari server; gunakan Query untuk itu.
- Reset UI-state saat berganti rute (contoh: dari Checkout ke PaymentStatus).
- Hindari global state berlebihan; scope-kan per feature bila perlu.
5.3 Error Handling & Logging
- Mapping error backend ke pesan user-friendly di
features/payments/components/ErrorBanner.tsx. - Log teknis (correlation id, status) di
lib/logger.ts; hindari menyimpan data sensitif. - Tampilkan CTA standar:
Retry,Back, atauContact Supportsesuai konteks.
5.4 Kontrak API & Mutations
- Semua
chargedipanggil via backend (/api/payments/charge) denganpayment_typedan parameter metode (VA/QRIS/Card/Cstore). Server Key tetap di backend. - Untuk kartu: frontend mengambil
token_idvia script 3DS, mengirim ke backend untukchargedenganauthentication: truebila 3DS diaktifkan. - Mutations mengembalikan
order_idatau detail awal; UI lalu menavigasi kePaymentStatusdan memulai polling.
Elicit Options (1–9)
- Proceed ke API Integration (kontrak endpoints & services)
- Risk & Challenge (ketahanan polling dan dampak UX)
- Structural Analysis (batas UI-state vs server-state)
- Devil’s Advocate (apakah store terpisah perlu diganti Context-only?)
- Multi-Persona (PO/SM/Dev/QA sudut pandang)
- Creative Exploration (push vs polling: SSE/WebSocket pros/cons)
- 2025 Techniques (background sync & offline cache relevansi)
- Process Control (standar error mapping & logging ringan)
- Reflective Review (keselarasan dengan PRD status & rekonsiliasi)
Select 1–9 or just type your question/feedback:
4. Component Standards
Tujuan: menyelaraskan pola pembuatan komponen agar konsisten, mudah dirawat, dan dapat ditinjau dengan QA manual.
4.1 Component Template (TypeScript + A11y + Tailwind)
Gunakan fungsi komponen dengan props bertipe jelas, aksesibilitas dasar, dan kelas Tailwind terstandar.
// components/Button.tsx
import React from 'react';
export type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
'aria-label'?: string;
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
...aria
}) => {
const base =
'inline-flex items-center justify-center rounded-md font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors';
const sizes: Record<NonNullable<ButtonProps['size']>, string> = {
sm: 'px-2.5 py-1.5 text-xs',
md: 'px-3 py-2 text-sm',
lg: 'px-4 py-2.5 text-base',
};
const variants: Record<NonNullable<ButtonProps['variant']>, string> = {
primary:
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 disabled:bg-blue-400',
secondary:
'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400 disabled:bg-gray-200',
danger:
'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 disabled:bg-red-400',
};
return (
<button
type="button"
className={`${base} ${sizes[size]} ${variants[variant]}`}
disabled={disabled}
onClick={onClick}
{...aria}
>
{children}
</button>
);
};
export default Button;
Contoh komponen formulir ringan (gunakan React Hook Form di page yang memakai form):
// components/FormField.tsx
import React from 'react';
export type FormFieldProps = {
label: string;
name: string;
type?: 'text' | 'number';
value: string | number;
onChange: (value: string) => void;
error?: string;
required?: boolean;
};
export const FormField: React.FC<FormFieldProps> = ({
label,
name,
type = 'text',
value,
onChange,
error,
required,
}) => {
const id = `field-${name}`;
return (
<div className="space-y-1">
<label htmlFor={id} className="text-sm font-medium text-gray-700">
{label} {required && <span className="text-red-600">*</span>}
</label>
<input
id={id}
name={name}
type={type}
className="block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500"
value={String(value)}
onChange={(e) => onChange(e.target.value)}
aria-invalid={Boolean(error)}
aria-describedby={error ? `${id}-error` : undefined}
/>
{error && (
<p id={`${id}-error`} className="text-xs text-red-600">
{error}
</p>
)}
</div>
);
};
Prinsip umum:
- Props selalu bertipe eksplisit; hindari
any. - Aksesibilitas: gunakan
aria-*,htmlFor, dan label jelas. - Tailwind untuk layout/warna; gunakan CSS Modules untuk gaya spesifik komponen bila perlu.
- Komponen presentasional tidak mengakses API langsung; logika data di layer
services/atau hooks.
4.2 Penamaan & Konvensi
- Components: PascalCase untuk nama file dan komponen (
PaymentSummary.tsx). - Hooks:
useCamelCaseuntuk nama hook (usePaymentStatus.ts). Hanya named exports. - Services: file camelCase berakhiran
Servicebila berkelas atau fungsi di satu file (paymentsService.ts). Named exports. - Stores (state):
paymentsStore.ts,uiStore.tsdengan named exports. - Types: nama tipe PascalCase (
Payment,PaymentStatus). Filetypes/payment.ts(lowercase singular). - Pages: PascalCase folder, file
index.tsxper page; komponen utama bernama sama dengan folder. - Routes:
routes.tsxsebagai pusat deklarasi rute. - CSS Modules:
Component.module.cssuntuk gaya spesifik; global distyles/. - Icons/assets: kebab-case (
credit-card.svg), impor dariassets/icons. - Test (opsional):
Component.test.tsxberdekatan dengan file sumber; karena QA manual, gunakan hanya untuk unit kecil yang berisiko.
Anti-pattern yang dihindari:
- Komponen melakukan fetch langsung ke Midtrans; gunakan
services/paymentsServiceatau hooks. - State global tanpa kontrol (gunakan TanStack Query untuk server-state, store terpisah untuk UI-state).
- Default export untuk hooks/services (prefer named export untuk tree-shaking dan konsistensi).
Rasional:
- Template tipe + a11y mempercepat review QA manual dan mengurangi regresi UI.
- Konvensi penamaan konsisten mempermudah navigasi struktur feature-first yang selaras dengan PRD.
- Pemisahan presentational vs data logic mendukung keterujian manual dan perawatan.
Elicit Options (1–9)
- Proceed ke State Management (server-state vs UI-state)
- Risk & Challenge (konsekuensi konvensi terhadap kecepatan dev)
- Structural Analysis (komponen presentasional vs container)
- Devil’s Advocate (apakah Tailwind + CSS Modules berlebih?)
- Multi-Persona (PO/SM/Dev/QA sudut pandang)
- Creative Exploration (atomic design vs feature-first)
- 2025 Techniques (server components/partial hydration relevansi)
- Process Control (standarisasi lint/format tanpa CI)
- Reflective Review (cek keselarasan dengan PRD Epics 1–3)
Select 1–9 or just type your question/feedback:
3. Project Structure
Struktur direktori yang diusulkan (Vite + React + TS) untuk memenuhi PRD (Checkout, VA/QRIS/Kartu, Status, Riwayat), TanStack Query (polling/retry), dan Tailwind CSS.
frontend/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .env.example
├── tailwind.config.js
├── postcss.config.js
├── src/
│ ├── main.tsx
│ ├── app/
│ │ ├── App.tsx
│ │ ├── routes.tsx
│ │ └── providers/
│ │ ├── QueryProvider.tsx
│ │ └── ThemeProvider.tsx
│ ├── pages/
│ │ ├── Checkout/
│ │ │ ├── CheckoutPage.tsx
│ │ │ └── components/
│ │ │ ├── MethodSelector.tsx
│ │ │ └── SummaryPanel.tsx
│ │ ├── PaymentStatus/
│ │ │ └── PaymentStatusPage.tsx
│ │ └── TransactionHistory/
│ │ └── TransactionHistoryPage.tsx
│ ├── features/
│ │ └── payments/
│ │ ├── VA/
│ │ │ ├── VAInstructions.tsx
│ │ │ └── VAInfoCard.tsx
│ │ ├── QRIS/
│ │ │ ├── QRISCanvas.tsx
│ │ │ └── QRISCountdown.tsx
│ │ ├── Card/
│ │ │ ├── CardForm.tsx
│ │ │ └── ThreeDSFlow.tsx
│ │ ├── components/
│ │ │ ├── ErrorBanner.tsx
│ │ │ ├── LoadingSkeleton.tsx
│ │ │ └── StatusBadge.tsx
│ │ ├── hooks/
│ │ │ ├── usePaymentStatus.ts
│ │ │ └── useCopyToClipboard.ts
│ │ ├── services/
│ │ │ ├── apiClient.ts
│ │ │ └── paymentsService.ts
│ │ ├── types/
│ │ │ └── payment.ts
│ │ └── store/
│ │ └── uiStore.ts
│ ├── components/
│ │ ├── Button.tsx
│ │ ├── Modal.tsx
│ │ └── FormField.tsx
│ ├── lib/
│ │ ├── logger.ts
│ │ └── accessibility.ts
│ ├── styles/
│ │ ├── index.css
│ │ └── themes.css
│ ├── assets/
│ │ └── icons/
│ ├── utils/
│ │ ├── formatting.ts
│ │ └── validation.ts
│ └── config/
│ └── env.ts
└── README.md
Rationale
- Memetakan PRD Epics ke struktur:
pages/Checkout(pemilihan metode & create transaksi),pages/PaymentStatus(status real-time),pages/TransactionHistory(riwayat pengguna). features/paymentsmengelompokkan komponen spesifik per-metode (VA/QRIS/Kartu), hooks (status/polling/copy), services (API client + payment service) agar mudah di-maintain.- TanStack Query di
providers/QueryProvider.tsxuntuk cache/polling/retry terpusat;services/apiClient.tsuntuk interceptors (auth, error mapping) dan konfigurasiVITE_API_BASE_URL. - Tailwind + CSS Modules: utilitas cepat + modularitas untuk komponen kompleks;
styles/themes.cssuntuk variabel global. - Observability ringan di
lib/logger.ts(correlation id pada event UI) dan komponenErrorBanner/StatusBadgekonsisten. .env.examplemendokumentasikan variabelVITE_...(frontend) terpisah dari secret backend;config/env.tsmembaca dan memvalidasi.
Opsi Elicitasi (1–9)
- Proceed ke Component Standards (template dan konvensi)
- Structural Analysis (cek keseimbangan pages vs features)
- Risk & Challenge (kompleksitas maintainability dan QA manual)
- Devil’s Advocate (apakah struktur terlalu granular?)
- Multi-Persona (PO/SM/Dev/QA sudut pandang)
- Creative Exploration (varian struktur: feature-first vs route-first)
- 2025 Techniques (module federation/partial hydration relevansi)
- Process Control (tandai asumsi & batasan eksplisit)
- Reflective Review (keselarasan dengan PRD & arsitektur backend)
Select 1–9 or just type your question/feedback: