1015 lines
41 KiB
Markdown
1015 lines
41 KiB
Markdown
# 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)
|
||
1. Proceed ke bagian berikutnya (Project Structure)
|
||
2. Risk & Challenge Analysis untuk pilihan stack
|
||
3. Structural Analysis (komponen vs halaman vs services)
|
||
4. Devil’s Advocate (tantangan stack ini untuk manual QA)
|
||
5. Multi-Persona (PO/SM/Dev/QA) feedback cepat
|
||
6. Creative Exploration (alternatif: Vue/Nuxt atau Angular)
|
||
7. 2025 Techniques (SSR-lite/Partial Hydration pertimbangan)
|
||
8. Process Control (tandai asumsi dan batasan eksplisit)
|
||
9. 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 gunakan `redirect_url` eksternal.
|
||
- Fallback: `*` — NotFound.
|
||
|
||
### 7.2 Konfigurasi React Router (v6)
|
||
|
||
```tsx
|
||
// 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`, setelah `charge` sukses (menerima `order_id`), arahkan ke `/payments/:orderId/status`.
|
||
- Simpan `payment_type` di `location.state` atau 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/challenge` yang menangani proses lalu kembali ke `/payments/:orderId/status`.
|
||
|
||
Contoh helper navigasi:
|
||
|
||
```ts
|
||
// 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`.
|
||
- Lindungi `/history` dengan batasan sederhana (misal hanya read-only; tidak memerlukan auth jika PRD tidak mensyaratkan).
|
||
- Toleransi refresh: halaman status dapat dibuka ulang langsung selama `order_id` valid.
|
||
|
||
### 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-Id` untuk tracing ringan di UI.
|
||
|
||
---
|
||
|
||
Elicit Options (Routing → Next)
|
||
1. Lanjut ke Styling (Tailwind arsitektur dan utility patterns)
|
||
2. Tambah Testing (rencana manual QA dan template kasus uji)
|
||
3. Environment Config (env vars & build/dev commands)
|
||
4. Developer Standards (lint, format, commit hygiene)
|
||
5. Error UX polish (banner, retry, deep link handling)
|
||
6. 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.
|
||
|
||
```ts
|
||
// 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.
|
||
|
||
```css
|
||
/* 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`/`cn` helper.
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```tsx
|
||
// 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.
|
||
|
||
```ts
|
||
// 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.
|
||
|
||
```css
|
||
/* 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; }
|
||
```
|
||
|
||
```tsx
|
||
// 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 `dark` di tag `html`/`body`; toggling dilakukan oleh provider tema UI (lihat State Management bagian UI-store).
|
||
- Gambar/ikon: pilih versi gelap/terang atau gunakan `opacity`/`mix-blend` untuk adaptasi.
|
||
|
||
### 8.6 Aksesibilitas Visual
|
||
|
||
- Fokus terlihat: gunakan `.focus-ring` dan `focus-visible` agar 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-tailwindcss` untuk pengurutan class otomatis (opsional, non-blocking).
|
||
- Pastikan purge/content paths tepat agar ukuran CSS hasil build tetap kecil.
|
||
|
||
---
|
||
|
||
Elicit Options (Styling → Next)
|
||
1. Testing (kasus uji manual dan template skenario)
|
||
2. Environment Config (env vars, build/dev command)
|
||
3. Developer Standards (lint/format/commit hygiene)
|
||
4. Error UX polish (banner, retry, deep link)
|
||
5. Performance (code-splitting, prefetch assets)
|
||
6. 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'` (tanpa `bank_transfer` body)
|
||
- 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, ... } }`
|
||
- Response (disederhanakan untuk UI):
|
||
- Bank Transfer: `{ order_id, payment_type:'bank_transfer', bank, va_number, status:'pending', expiry_time }` (backend menormalkan `va_numbers` menjadi satu `va_number`)
|
||
- Permata VA: `{ order_id, payment_type:'permata', va_number, status:'pending', expiry_time }` (backend menormalkan `permata_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 }`
|
||
- `GET /api/payments/:orderId/status`
|
||
- Response: `{ order_id, payment_type, status: 'pending' | 'settlement' | 'deny' | 'cancel' | 'expire', transaction_time?, gross_amount?, fraud_status? }`
|
||
- `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 endpoint `status`.
|
||
|
||
Catatan keamanan:
|
||
- Server Key hanya di backend; frontend memakai Client Key untuk 3DS script.
|
||
- Backend memvalidasi `order_id` untuk idempotensi; frontend boleh mengirim atau backend dapat membuatkan.
|
||
- Header standar: `X-Correlation-Id` untuk tracing.
|
||
|
||
### 6.2 Frontend Services (contracts)
|
||
|
||
`services/apiClient.ts` — klien HTTP berbasis Axios (atau fetch) dengan base URL dari `VITE_API_BASE_URL`.
|
||
|
||
```ts
|
||
// 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.
|
||
|
||
```ts
|
||
// 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/api`
|
||
- `VITE_MIDTRANS_CLIENT_KEY=YOUR_CLIENT_KEY`
|
||
- `VITE_MIDTRANS_ENV=sandbox` (atau `production`)
|
||
- Komponen loader script 3DS:
|
||
|
||
```tsx
|
||
// 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` (duplikasi `order_id`)
|
||
- 5xx: `SERVER_ERROR`
|
||
- UI menampilkan pesan ramah di `ErrorBanner` dan menyediakan CTA `Retry`, `Back`, `Contact Support` sesuai konteks.
|
||
- Logging ringan di `lib/logger.ts` mencatat `X-Correlation-Id` untuk pelacakan.
|
||
|
||
### 6.5 Alur per Metode (Ringkas)
|
||
|
||
- VA:
|
||
- `chargeVA(bank)` → tampilkan `va_number` + instruksi; arahkan ke `PaymentStatus`; polling hingga `settlement/expire`.
|
||
- Card (3DS):
|
||
- Muat 3DS script → `MidtransNew3ds.getCardToken(...)` → kirim `token_id` ke backend via `chargeCard(token)` → arahkan ke `PaymentStatus`; status bisa `challenge` sebelum `settlement`.
|
||
- QRIS/GoPay:
|
||
- `chargeQRIS()` → tampilkan `qr_url` (atau deeplink untuk mobile) + countdown; arahkan ke `PaymentStatus`; polling status.
|
||
- Cstore:
|
||
- `chargeCstore(store)` → tampilkan `payment_code` + instruksi; arahkan ke `PaymentStatus`; 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 satu `va_number` untuk UI.
|
||
- Permata: gunakan `payment_type: "permata"` (bukan `bank_transfer`); response field `permata_va_number` dinormalisasi menjadi `va_number` [0].
|
||
- Mandiri Bill Payment (E-Channel): gunakan `payment_type: "echannel"` dengan `echannel.{bill_info1,bill_info2}`; response berisi `bill_key` dan `biller_code` [0].
|
||
|
||
- Card (3DS)
|
||
- Frontend mengambil `token_id` via `midtrans-new-3ds` script (Client Key) [1].
|
||
- Backend Charge: `payment_type: "credit_card"`, `credit_card.{ token_id, authentication }` [1].
|
||
- Response dapat mengandung `redirect_url` (untuk challenge) dan `transaction_status` seperti `pending`, `challenge`, `capture`/`settlement`; UI menambahkan dukungan `capture` [1].
|
||
|
||
- E-Wallet/QRIS (GoPay)
|
||
- Request: `payment_type: "gopay"`; optional `gopay.{ enable_callback, callback_url }` [2].
|
||
- Response: `actions` (QR URL untuk desktop, deeplink untuk mobile) dan `transaction_status: pending`; backend menormalkan ke `qr_url` dan `deeplink_url` untuk konsumsi UI [2].
|
||
|
||
- 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].
|
||
|
||
- 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)
|
||
1. Proceed ke Routing (navigasi Checkout → Status → Riwayat)
|
||
2. Risk & Challenge (kontrak API dan dampak QA manual)
|
||
3. Structural Analysis (abstraksi services dan tipe respons)
|
||
4. Devil’s Advocate (apakah satu endpoint `charge` terlalu generik?)
|
||
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
|
||
6. Creative Exploration (layer adapter untuk future payment methods)
|
||
7. 2025 Techniques (SSE/WebSocket untuk status push)
|
||
8. Process Control (idempotensi & correlation id praktik)
|
||
9. 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 `charge` sukses, arahkan ke `PaymentStatus` dan mulai polling `status`.
|
||
- Hentikan polling saat mencapai terminal state (`settlement`, `deny`, `cancel`, `expire`).
|
||
- Invalidate status jika webhook backend mengubah status (opsional: gunakan indikator versi/timestamp dari backend).
|
||
|
||
Contoh hook status dengan polling kondisional:
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```ts
|
||
// 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`, atau `Contact Support` sesuai konteks.
|
||
|
||
### 5.4 Kontrak API & Mutations
|
||
|
||
- Semua `charge` dipanggil via backend (`/api/payments/charge`) dengan `payment_type` dan parameter metode (VA/QRIS/Card/Cstore). Server Key tetap di backend.
|
||
- Untuk kartu: frontend mengambil `token_id` via script 3DS, mengirim ke backend untuk `charge` dengan `authentication: true` bila 3DS diaktifkan.
|
||
- Mutations mengembalikan `order_id` atau detail awal; UI lalu menavigasi ke `PaymentStatus` dan memulai polling.
|
||
|
||
---
|
||
|
||
Elicit Options (1–9)
|
||
1. Proceed ke API Integration (kontrak endpoints & services)
|
||
2. Risk & Challenge (ketahanan polling dan dampak UX)
|
||
3. Structural Analysis (batas UI-state vs server-state)
|
||
4. Devil’s Advocate (apakah store terpisah perlu diganti Context-only?)
|
||
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
|
||
6. Creative Exploration (push vs polling: SSE/WebSocket pros/cons)
|
||
7. 2025 Techniques (background sync & offline cache relevansi)
|
||
8. Process Control (standar error mapping & logging ringan)
|
||
9. 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.
|
||
|
||
```tsx
|
||
// 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):
|
||
|
||
```tsx
|
||
// 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: `useCamelCase` untuk nama hook (`usePaymentStatus.ts`). Hanya named exports.
|
||
- Services: file camelCase berakhiran `Service` bila berkelas atau fungsi di satu file (`paymentsService.ts`). Named exports.
|
||
- Stores (state): `paymentsStore.ts`, `uiStore.ts` dengan named exports.
|
||
- Types: nama tipe PascalCase (`Payment`, `PaymentStatus`). File `types/payment.ts` (lowercase singular).
|
||
- Pages: PascalCase folder, file `index.tsx` per page; komponen utama bernama sama dengan folder.
|
||
- Routes: `routes.tsx` sebagai pusat deklarasi rute.
|
||
- CSS Modules: `Component.module.css` untuk gaya spesifik; global di `styles/`.
|
||
- Icons/assets: kebab-case (`credit-card.svg`), impor dari `assets/icons`.
|
||
- Test (opsional): `Component.test.tsx` berdekatan 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/paymentsService` atau 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)
|
||
1. Proceed ke State Management (server-state vs UI-state)
|
||
2. Risk & Challenge (konsekuensi konvensi terhadap kecepatan dev)
|
||
3. Structural Analysis (komponen presentasional vs container)
|
||
4. Devil’s Advocate (apakah Tailwind + CSS Modules berlebih?)
|
||
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
|
||
6. Creative Exploration (atomic design vs feature-first)
|
||
7. 2025 Techniques (server components/partial hydration relevansi)
|
||
8. Process Control (standarisasi lint/format tanpa CI)
|
||
9. 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/payments` mengelompokkan 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.tsx` untuk cache/polling/retry terpusat; `services/apiClient.ts` untuk interceptors (auth, error mapping) dan konfigurasi `VITE_API_BASE_URL`.
|
||
- Tailwind + CSS Modules: utilitas cepat + modularitas untuk komponen kompleks; `styles/themes.css` untuk variabel global.
|
||
- Observability ringan di `lib/logger.ts` (correlation id pada event UI) dan komponen `ErrorBanner/StatusBadge` konsisten.
|
||
- `.env.example` mendokumentasikan variabel `VITE_...` (frontend) terpisah dari secret backend; `config/env.ts` membaca dan memvalidasi.
|
||
|
||
Opsi Elicitasi (1–9)
|
||
1. Proceed ke Component Standards (template dan konvensi)
|
||
2. Structural Analysis (cek keseimbangan pages vs features)
|
||
3. Risk & Challenge (kompleksitas maintainability dan QA manual)
|
||
4. Devil’s Advocate (apakah struktur terlalu granular?)
|
||
5. Multi-Persona (PO/SM/Dev/QA sudut pandang)
|
||
6. Creative Exploration (varian struktur: feature-first vs route-first)
|
||
7. 2025 Techniques (module federation/partial hydration relevansi)
|
||
8. Process Control (tandai asumsi & batasan eksplisit)
|
||
9. Reflective Review (keselarasan dengan PRD & arsitektur backend)
|
||
|
||
Select 1–9 or just type your question/feedback: |