From e1f989447b6b408020d1bf0502354375f3bd69b5 Mon Sep 17 00:00:00 2001 From: TengkuAchmad Date: Fri, 14 Nov 2025 15:13:46 +0700 Subject: [PATCH] feat: implement toast notifications and update UI components refactor: remove dark mode styles and simplify UI components style: update color scheme and branding to new blue theme feat(toast): add toast notification system for user feedback fix: correct merchant name and update payment sheet styling docs: update comments and remove unused code --- src/app/AppLayout.tsx | 10 ++-- src/components/alert/Alert.tsx | 2 +- src/components/ui/button.tsx | 6 +- src/components/ui/toast.tsx | 57 +++++++++++++++++++ .../payments/components/BankTransferPanel.tsx | 37 ++++++------ .../components/BcaInstructionList.tsx | 14 ++--- .../payments/components/CStorePanel.tsx | 21 ++++--- .../payments/components/CardPanel.tsx | 23 ++++---- .../payments/components/GoPayPanel.tsx | 33 ++++++----- .../components/InlinePaymentStatus.tsx | 20 +++---- .../components/PaymentInstructions.tsx | 4 +- .../payments/components/PaymentMethodList.tsx | 16 +++--- .../payments/components/PaymentSheet.tsx | 20 ++++--- .../payments/components/TrustStrip.tsx | 10 ++-- src/pages/CheckoutPage.tsx | 24 ++++---- src/pages/DemoStorePage.tsx | 4 +- src/pages/NotFoundPage.tsx | 2 +- src/pages/PayPage.tsx | 34 +++++------ src/pages/PaymentStatusPage.tsx | 24 ++++---- src/styles/globals.css | 14 ++--- tailwind.config.ts | 22 +++---- 21 files changed, 224 insertions(+), 173 deletions(-) create mode 100644 src/components/ui/toast.tsx diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index 96c10a6..cee70b9 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -1,9 +1,10 @@ import { Link, Outlet } from 'react-router-dom' +import { ToastHost } from '../components/ui/toast' export function AppLayout() { return (
-
+ {/*
Core Midtrans CIFO
-
+
*/}
-
+ + {/*
Brand: Merah–Hitam–Putih • Sandbox UI skeleton -
+
*/}
) } \ No newline at end of file diff --git a/src/components/alert/Alert.tsx b/src/components/alert/Alert.tsx index 1b2f313..e63fd9e 100644 --- a/src/components/alert/Alert.tsx +++ b/src/components/alert/Alert.tsx @@ -4,7 +4,7 @@ export function Alert({ title, children }: { title: string; children?: React.Rea
{title}
- {children ?
{children}
: null} + {children ?
{children}
: null}
) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0a8eb36..5cea9a0 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -2,13 +2,13 @@ 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: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', + '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 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', + secondary: 'bg-white text-black border border-black/10 hover:bg-black/5', + outline: 'border border-black text-black hover:bg-black/5', }, size: { sm: 'h-8 px-3', md: 'h-10 px-4', lg: 'h-11 px-6' }, }, diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..0030c76 --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,57 @@ +import React from 'react' + +type ToastKind = 'info' | 'success' | 'error' +type ToastItem = { id: number; message: string; kind: ToastKind } + +let nextId = 1 +const subs = new Set<(t: ToastItem) => void>() + +export const toast = { + show(message: string, kind: ToastKind = 'info') { + const item: ToastItem = { id: nextId++, message, kind } + subs.forEach((fn) => fn(item)) + }, + info(message: string) { toast.show(message, 'info') }, + success(message: string) { toast.show(message, 'success') }, + error(message: string) { toast.show(message, 'error') }, +} + +export function ToastHost() { + const [items, setItems] = React.useState([]) + + React.useEffect(() => { + function onPush(t: ToastItem) { + setItems((prev) => [...prev, t]) + // auto-remove after 3.5s + setTimeout(() => { + setItems((prev) => prev.filter((x) => x.id !== t.id)) + }, 3500) + } + subs.add(onPush) + return () => { subs.delete(onPush) } + }, []) + + if (!items.length) return null + + return ( +
+ {items.map((it) => ( +
+ {it.message} +
+ ))} +
+ ) +} \ No newline at end of file diff --git a/src/features/payments/components/BankTransferPanel.tsx b/src/features/payments/components/BankTransferPanel.tsx index 853cbee..d77780b 100644 --- a/src/features/payments/components/BankTransferPanel.tsx +++ b/src/features/payments/components/BankTransferPanel.tsx @@ -3,11 +3,11 @@ import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from './PaymentInstructions' import { BcaInstructionList } from './BcaInstructionList' -import { TrustStrip } from './TrustStrip' import { type BankKey } from './PaymentLogos' import { postCharge } from '../../../services/api' import { Alert } from '../../../components/alert/Alert' import { InlinePaymentStatus } from './InlinePaymentStatus' +import { toast } from '../../../components/ui/toast' // Global guard to prevent duplicate auto-charge across StrictMode double-mounts const attemptedChargeKeys = new Set() @@ -29,7 +29,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, function copy(text: string, label: string) { if (!text) return navigator.clipboard?.writeText(text) - alert(`${label} disalin: ${text}`) + toast.success(`${label} disalin: ${text}`) } // Auto-create VA immediately when a bank is selected (runs once per selection) React.useEffect(() => { @@ -125,7 +125,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, } run() return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selected]) // Auto-show instructions when BCA is selected to reduce confusion @@ -137,43 +137,43 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, return (
-
Transfer bank
+
Transfer Bank
{selected && (
- Bank: - {selected.toUpperCase()} + Bank: + {selected.toUpperCase()}
)} -
VA dibuat otomatis sesuai bank pilihan Anda.
+
VA dibuat otomatis sesuai bank pilihan Anda.
{errorMessage && ( {errorMessage} )} {selected && (
-
+
Virtual Account
-
+
{vaCode ? ( Nomor VA: - {vaCode} + {vaCode} ) : ( - {busy && } + {busy && } {busy ? 'Membuat VA…' : 'VA akan muncul otomatis setelah transaksi dibuat.'} )} {billKey && ( - Bill Key: {billKey} + Bill Key: {billKey} )} {billerCode && ( - Biller Code: {billerCode} + Biller Code: {billerCode} )}
- - + +
@@ -187,7 +187,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, {selected === 'bca' ? ( ) : ( -
+
Instruksi pembayaran
@@ -195,7 +195,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
)} {locked && ( -
Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.
+
Metode terkunci. Gunakan kode VA/bill key untuk menyelesaikan pembayaran.
)}
{(!vaCode || errorMessage) && ( @@ -280,7 +280,7 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, > {busy ? ( - + Membuat VA… ) : 'Buat VA'} @@ -295,7 +295,6 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated, Buka halaman status
-
) } diff --git a/src/features/payments/components/BcaInstructionList.tsx b/src/features/payments/components/BcaInstructionList.tsx index ea969de..c029b6f 100644 --- a/src/features/payments/components/BcaInstructionList.tsx +++ b/src/features/payments/components/BcaInstructionList.tsx @@ -5,13 +5,13 @@ type TabKey = 'mobile' | 'atm' | 'ib' export function BcaInstructionList() { const [tab, setTab] = React.useState('mobile') return ( -
+
Instruksi BCA
@@ -31,7 +31,7 @@ export function BcaInstructionList() { aria-selected={tab === 'atm'} aria-controls="panel-bca-atm" onClick={() => setTab('atm')} - className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'atm' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`} + className={`px-3 py-2 text-xs font-medium focus:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${tab === 'atm' ? 'bg-black text-white' : 'bg-transparent hover:bg-gray-100'}`} > ATM BCA @@ -42,7 +42,7 @@ export function BcaInstructionList() { aria-selected={tab === 'ib'} aria-controls="panel-bca-ib" onClick={() => setTab('ib')} - className={`px-3 py-2 text-xs font-medium focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${tab === 'ib' ? 'bg-black text-white' : 'bg-transparent hover:bg-black/5 dark:hover:bg-white/10'}`} + className={`px-3 py-2 text-xs font-medium focus:outline-none focus-visible:ring-[#2563EB] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${tab === 'ib' ? 'bg-black text-white' : 'bg-transparent hover:bg-gray-100'}`} > KlikBCA @@ -63,7 +63,7 @@ export function BcaInstructionList() {
)}
-
+
Catatan: Gunakan label "Nomor VA" secara konsisten di semua metode.
@@ -111,7 +111,7 @@ function StepsKlikBca() { function StepList({ steps }: { steps: string[] }) { return ( -
    +
      {steps.map((s, i) => (
    1. {s}
    2. ))} diff --git a/src/features/payments/components/CStorePanel.tsx b/src/features/payments/components/CStorePanel.tsx index 6c8724a..e7d1b82 100644 --- a/src/features/payments/components/CStorePanel.tsx +++ b/src/features/payments/components/CStorePanel.tsx @@ -1,8 +1,8 @@ import { Button } from '../../../components/ui/button' +import { toast } from '../../../components/ui/toast' import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from './PaymentInstructions' -import { TrustStrip } from './TrustStrip' import { postCharge } from '../../../services/api' import { InlinePaymentStatus } from './InlinePaymentStatus' @@ -36,7 +36,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul if (typeof res?.store === 'string') setStoreFromRes(res.store) } } catch (e) { - if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`) + if (!cancelled) toast.error(`Gagal membuat kode pembayaran: ${(e as Error).message}`) } finally { if (!cancelled) setBusy(false) cstoreTasks.delete(chargeKey) @@ -60,7 +60,7 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul if (typeof res?.store === 'string') setStoreFromRes(res.store) } } catch (e) { - if (!cancelled) alert(`Gagal membuat kode pembayaran: ${(e as Error).message}`) + if (!cancelled) toast.error(`Gagal membuat kode pembayaran: ${(e as Error).message}`) attemptedCStoreKeys.delete(chargeKey) } finally { if (!cancelled) setBusy(false) @@ -74,14 +74,14 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul function copy(text: string, label: string) { if (!text) return navigator.clipboard?.writeText(text) - alert(`${label} disalin: ${text}`) + toast.success(`${label} disalin: ${text}`) } return (
      Convenience Store
      {selected && ( -
      Toko dipilih: {selected.toUpperCase()}
      +
      Toko dipilih: {selected.toUpperCase()}
      )}
      -
) } \ No newline at end of file diff --git a/src/features/payments/components/CardPanel.tsx b/src/features/payments/components/CardPanel.tsx index a7940c6..ebb5c75 100644 --- a/src/features/payments/components/CardPanel.tsx +++ b/src/features/payments/components/CardPanel.tsx @@ -2,13 +2,13 @@ import { Button } from '../../../components/ui/button' import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from './PaymentInstructions' -import { TrustStrip } from './TrustStrip' import { CardLogosRow } from './PaymentLogos' import { ensureMidtrans3ds, getCardToken, authenticate3ds } from '../lib/midtrans3ds' import { Logger } from '../../../lib/logger' import { Env } from '../../../lib/env' import { postCharge } from '../../../services/api' import { InlinePaymentStatus } from './InlinePaymentStatus' +import { toast } from '../../../components/ui/toast' export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) { const nav = usePaymentNavigation() @@ -41,16 +41,16 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
@@ -69,7 +69,7 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde {showGuide && } {locked && ( -
Metode terkunci. Lanjutkan verifikasi 3DS/OTP pada kartu Anda.
+
Metode terkunci. Lanjutkan verifikasi 3DS/OTP pada kartu Anda.
)}
{/* Status inline dengan polling otomatis */} -
) } \ No newline at end of file diff --git a/src/features/payments/components/GoPayPanel.tsx b/src/features/payments/components/GoPayPanel.tsx index 3d87008..38bb405 100644 --- a/src/features/payments/components/GoPayPanel.tsx +++ b/src/features/payments/components/GoPayPanel.tsx @@ -2,10 +2,10 @@ import { Button } from '../../../components/ui/button' import { usePaymentNavigation } from '../lib/navigation' import React from 'react' import { PaymentInstructions } from './PaymentInstructions' -import { TrustStrip } from './TrustStrip' import { GoPayLogosRow } from './PaymentLogos' import { postCharge } from '../../../services/api' import { InlinePaymentStatus } from './InlinePaymentStatus' +import { toast } from '../../../components/ui/toast' // Global guards/tasks to stabilize QR generation across StrictMode remounts const attemptedChargeKeys = new Set() @@ -48,7 +48,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord a.target = '_blank' a.click() } else { - alert('QR belum tersedia. Klik "Buat QR" terlebih dulu.') + toast.error('QR belum tersedia. Klik "Buat QR" terlebih dulu.') } } return ( @@ -68,13 +68,13 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
GoPay / QRIS
- Mode: -
+ Mode: +
@@ -82,25 +82,25 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord type="button" onClick={() => setMode('qris')} aria-pressed={mode==='qris'} - className={`px-2 py-1 focus:outline-none ring-[#2563EB] ring-3 ring-offset-2 ring-offset-white dark:ring-offset-black transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white dark:bg-black text-black dark:text-white hover:bg-black/10 dark:hover:bg-white/10'}`} + className={`px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white transition ${mode==='qris' ? 'bg-black text-white' : 'bg-white text-black hover:bg-black/10'}`} > QRIS
-
-
Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}
-
+
+
Scan QR berikut menggunakan aplikasi {mode === 'gopay' ? 'GoPay' : 'QRIS'}
+
{mode === 'qris' && (!qrUrl || busy) ? ( - - + + Membuat QR… ) : qrUrl ? ( QR untuk pembayaran ) : null}
-
Mode: {mode.toUpperCase()}
+
Mode: {mode.toUpperCase()}
@@ -125,7 +125,7 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord />
{locked && ( -
Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.
+
Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.
)}
@@ -139,14 +139,13 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord > {busy ? ( - + Menuju status… ) : 'Buka halaman status'}
-
) } @@ -176,7 +175,7 @@ export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy, onChargeInitiated?.() } } catch (e) { - if (!cancelled) alert(`Gagal membuat QR: ${(e as Error).message}`) + if (!cancelled) toast.error(`Gagal membuat QR: ${(e as Error).message}`) } finally { if (!cancelled) { setBusy(false) @@ -205,7 +204,7 @@ export function GoPayPanel_AutoEffect({ orderId, amount, locked, mode, setBusy, onChargeInitiated?.() } } catch (e) { - if (!cancelled) alert(`Gagal membuat QR: ${(e as Error).message}`) + if (!cancelled) toast.error(`Gagal membuat QR: ${(e as Error).message}`) attemptedChargeKeys.delete(chargeKey) } finally { if (!cancelled) { diff --git a/src/features/payments/components/InlinePaymentStatus.tsx b/src/features/payments/components/InlinePaymentStatus.tsx index 2236725..80b5049 100644 --- a/src/features/payments/components/InlinePaymentStatus.tsx +++ b/src/features/payments/components/InlinePaymentStatus.tsx @@ -18,7 +18,7 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str const isFailure = ['deny', 'cancel', 'expire', 'refund', 'chargeback'].includes(status) return ( -
+
{/* Header minimal tanpa detail teknis */}
Status pembayaran
@@ -26,10 +26,10 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str {isLoading ? (
- + Mengecek pembayaran… -
Kami memeriksa otomatis setiap 3 detik.
+
Kami memeriksa otomatis setiap 3 detik.
) : error ? (
Gagal memuat status. Coba refresh.
@@ -45,9 +45,9 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
Pembayaran berhasil
{data?.grossAmount ? ( -
Total dibayar: {formatIDR(data.grossAmount)}
+
Total dibayar: {formatIDR(data.grossAmount)}
) : null} -
Terima kasih! Pesanan Anda sedang diproses.
+
Terima kasih! Pesanan Anda sedang diproses.
@@ -66,7 +66,7 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str
Pembayaran belum berhasil
-
Silakan coba lagi atau pilih metode lain.
+
Silakan coba lagi atau pilih metode lain.
@@ -75,18 +75,18 @@ export function InlinePaymentStatus({ orderId, method, compact }: { orderId: str ) : (
- + {/* hourglass/spinner icon */} - +
Menunggu pembayaran
-
Kami memeriksa otomatis setiap 3 detik sampai selesai.
+
Kami memeriksa otomatis setiap 3 detik sampai selesai.
{selected === it.key && renderPanel && ( -
+
{renderPanel(it.key)}
)} diff --git a/src/features/payments/components/PaymentSheet.tsx b/src/features/payments/components/PaymentSheet.tsx index 2f8d2fe..22331e6 100644 --- a/src/features/payments/components/PaymentSheet.tsx +++ b/src/features/payments/components/PaymentSheet.tsx @@ -28,7 +28,7 @@ export interface PaymentSheetProps { showStatusCTA?: boolean } -export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) { +export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) { const countdown = useCountdown(expireAt) const [expanded, setExpanded] = React.useState(true) return ( @@ -38,7 +38,7 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
- ZARA + SIMAYA
{merchantName}
@@ -57,17 +57,19 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, onClick={() => setExpanded((v) => !v)} className={`text-white/80 transition-transform ${expanded ? '' : 'rotate-180'} focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-white/80 focus-visible:ring-offset-2`} > - ˅ + + +
{/* Amount panel */} {expanded && ( -
+
-
Total
+
Total
{formatCurrencyIDR(amount)}
-
Order ID #{orderId}
+
Order ID #{orderId}
)} @@ -78,11 +80,11 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
{/* Sticky CTA (mobile-friendly) */} {showStatusCTA && ( -
+
Cek status pembayaran diff --git a/src/features/payments/components/TrustStrip.tsx b/src/features/payments/components/TrustStrip.tsx index e7e7398..5a0aef6 100644 --- a/src/features/payments/components/TrustStrip.tsx +++ b/src/features/payments/components/TrustStrip.tsx @@ -2,14 +2,14 @@ export function TrustStrip({ location = 'panel' }: { location?: 'panel' | 'sheet' }) { return ( -
+
🔒 Secure payments by Midtrans - Visa - Mastercard - JCB - Amex + Visa + Mastercard + JCB + Amex
) diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index bb10e2a..4e4f416 100644 --- a/src/pages/CheckoutPage.tsx +++ b/src/pages/CheckoutPage.tsx @@ -72,13 +72,13 @@ export function CheckoutPage() { )} - + {/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */} {currentStep === 1 && (
Konfirmasi data checkout