Phase 5.x payment revamp + Xendit Stage-8 prep
- Backend wraps idn-finlogos npm at /assets/payment-icons/<slug>.svg with
1y immutable cache. Mobile drops bundled SVGs (only placeholder remains)
and fetches via flutter_cache_manager. payment_methods.icon is now a
CSV of slugs; catalog emits icon_urls[]. CARDS tile renders Visa + MC +
JCB side by side.
- Per-method min/max amount bounds (BIGINT, nullable). Picker greys out
out-of-range tiles with subtitle; backend gates with INVALID_PAYMENT_AMOUNT
(422). Defense in depth against stale-catalog clients.
- Xendit channel codes corrected from authoritative docs
(BCA_VA -> BCA_VIRTUAL_ACCOUNT, CREDIT_CARD -> CARDS, ovo -> ovo-new,
shopeepay -> shopee-pay, ...). 18 methods x 5 groups seeded with
Xendit-published per-channel min/max.
- Re-runnable seed (ON CONFLICT DO NOTHING on payment_code + new unique
index on group name). Operator CC edits never clobbered across re-runs.
One-shot reset + inspect scripts under backend/.dev/.
- Customer redirect HTML pages at /payment/return/{success,failure},
brand-styled with "Buka HaloBestie" CTA firing halobestie:// deeplink.
URL scheme registered on Android (intent-filter w/ BROWSABLE on
MainActivity) and iOS (CFBundleURLTypes). Waiting-payment poller still
owns confirmation; deeplink just brings the activity to foreground.
- Control center payment-catalog page: min/max inputs + columns. Other
CC pages restyled with new theme tokens (separate work, bundled here).
169/169 backend tests pass. See requirement/phase5-payment-revamp-2026-05-27.md
for the full revamp doc. Stage 8 (E2E) still pending: webhook URL routing
decision + two client_app follow-ups (legacy /chat/request removal,
extension Custom Tab).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,18 @@ import { useState } from 'react'
|
||||
import { Outlet, NavLink } from 'react-router-dom'
|
||||
import { useAuth } from '../core/auth/AuthContext'
|
||||
import { apiClient } from '../core/api/api-client'
|
||||
import HBLogo from './ui/HBLogo'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: '/dashboard', label: 'Dashboard' },
|
||||
{ to: '/mitras', label: 'Mitra' },
|
||||
{ to: '/sessions', label: 'Sesi' },
|
||||
{ to: '/failed-pairings', label: 'Failed Pairings' },
|
||||
{ to: '/users', label: 'Users' },
|
||||
{ to: '/mitra-activity', label: 'Aktivitas Mitra' },
|
||||
{ to: '/payment-catalog', label: 'Payment Catalog' },
|
||||
{ to: '/settings', label: 'Settings' },
|
||||
]
|
||||
|
||||
const PasswordChangeForm = ({ onDone }) => {
|
||||
const [current, setCurrent] = useState('')
|
||||
@@ -34,18 +46,33 @@ const PasswordChangeForm = ({ onDone }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit} style={{ padding: 8, border: '1px solid #eee', marginTop: 8 }}>
|
||||
<input type="password" placeholder="Password lama" value={current}
|
||||
onChange={e => setCurrent(e.target.value)} required
|
||||
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
||||
<input type="password" placeholder="Password baru (min 8, huruf besar/kecil + angka)" value={next}
|
||||
onChange={e => setNext(e.target.value)} required minLength={8}
|
||||
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
||||
{error && <p style={{ color: 'red', margin: '4px 0', fontSize: 12 }}>{error}</p>}
|
||||
{success && <p style={{ color: 'green', margin: '4px 0', fontSize: 12 }}>Password berhasil diubah.</p>}
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button type="submit" disabled={saving}>{saving ? '...' : 'Simpan'}</button>
|
||||
<button type="button" onClick={onDone}>Tutup</button>
|
||||
<form onSubmit={submit} className="hb-card" style={{ marginTop: 10, padding: 14 }}>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password lama"
|
||||
value={current}
|
||||
onChange={(e) => setCurrent(e.target.value)}
|
||||
required
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password baru (min 8, huruf besar/kecil + angka)"
|
||||
value={next}
|
||||
onChange={(e) => setNext(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
{error && <p className="hb-error">{error}</p>}
|
||||
{success && <p className="hb-success">Password berhasil diubah.</p>}
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button type="submit" disabled={saving} style={{ flex: 1 }}>
|
||||
{saving ? 'Menyimpan…' : 'Simpan'}
|
||||
</button>
|
||||
<button type="button" className="hb-btn-secondary" onClick={onDone}>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
@@ -56,27 +83,111 @@ export default function Layout() {
|
||||
const [showPwForm, setShowPwForm] = useState(false)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh' }}>
|
||||
<nav style={{ width: 220, borderRight: '1px solid #eee', padding: 16 }}>
|
||||
<h2>Control Center</h2>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
<li><NavLink to="/dashboard">Dashboard</NavLink></li>
|
||||
<li><NavLink to="/mitras">Mitra</NavLink></li>
|
||||
<li><NavLink to="/sessions">Sesi</NavLink></li>
|
||||
<li><NavLink to="/failed-pairings">Failed Pairings</NavLink></li>
|
||||
<li><NavLink to="/users">Users</NavLink></li>
|
||||
<li><NavLink to="/mitra-activity">Aktivitas Mitra</NavLink></li>
|
||||
<li><NavLink to="/payment-catalog">Payment Catalog</NavLink></li>
|
||||
<li><NavLink to="/settings">Settings</NavLink></li>
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--hb-bg)' }}>
|
||||
<nav
|
||||
style={{
|
||||
width: 260,
|
||||
padding: '24px 20px',
|
||||
background: 'var(--hb-surface)',
|
||||
borderRight: '1px solid var(--hb-border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<HBLogo size={44} />
|
||||
<div style={{ lineHeight: 1.15 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'var(--hb-font-display)',
|
||||
fontWeight: 700,
|
||||
fontSize: 17,
|
||||
color: 'var(--hb-ink)',
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
Halo Bestie
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: 'var(--hb-brand-dark)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
Control Center
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
style={{
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<li key={item.to}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
className={({ isActive }) => `hb-nav-link${isActive ? ' active' : ''}`}
|
||||
>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div style={{ marginTop: 'auto', paddingTop: 16 }}>
|
||||
<p style={{ fontSize: 12 }}>{user?.email}</p>
|
||||
<button onClick={() => setShowPwForm(v => !v)} style={{ marginRight: 6 }}>Ganti password</button>
|
||||
<button onClick={logout}>Logout</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 16,
|
||||
borderTop: '1px solid var(--hb-border)',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="hb-soft"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
margin: '0 0 10px',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{user?.email}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="hb-btn-ghost"
|
||||
onClick={() => setShowPwForm((v) => !v)}
|
||||
style={{ flex: 1, minHeight: 36, fontSize: 13 }}
|
||||
>
|
||||
Ganti password
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hb-btn-secondary"
|
||||
onClick={logout}
|
||||
style={{ minHeight: 36, fontSize: 13 }}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
{showPwForm && <PasswordChangeForm onDone={() => setShowPwForm(false)} />}
|
||||
</div>
|
||||
</nav>
|
||||
<main style={{ flex: 1, padding: 24 }}>
|
||||
|
||||
<main style={{ flex: 1, padding: '28px 32px', minWidth: 0 }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user