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:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../../core/auth/AuthContext'
|
||||
import HBLogo from '../../components/ui/HBLogo'
|
||||
|
||||
const messageForError = (err) => {
|
||||
const code = err?.response?.data?.error?.code
|
||||
@@ -41,26 +42,93 @@ export default function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (authLoading) return <div style={{ padding: 24 }}>Loading...</div>
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div style={{ padding: 24, color: 'var(--hb-ink-soft)' }}>Loading…</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 360, margin: '100px auto', padding: 24 }}>
|
||||
<h1>Halo Bestie</h1>
|
||||
<h2>Control Center</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="cc-login-email">Email</label>
|
||||
<input id="cc-login-email" type="email" value={email} onChange={e => setEmail(e.target.value)} required style={{ display: 'block', width: '100%', marginBottom: 12 }} />
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background:
|
||||
'radial-gradient(1100px 540px at 50% -120px, var(--hb-brand-softer), transparent 70%), var(--hb-bg)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="hb-card"
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 380,
|
||||
padding: 32,
|
||||
boxShadow: 'var(--hb-shadow-card)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<HBLogo size={72} />
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h1 style={{ margin: 0, fontSize: 24 }}>Halo Bestie</h1>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--hb-brand-dark)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
Control Center
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="cc-login-password">Password</label>
|
||||
<input id="cc-login-password" type="password" value={password} onChange={e => setPassword(e.target.value)} required style={{ display: 'block', width: '100%', marginBottom: 12 }} />
|
||||
</div>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
<button type="submit" disabled={loading} style={{ width: '100%' }}>
|
||||
{loading ? 'Loading...' : 'Masuk'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="hb-form-row">
|
||||
<label htmlFor="cc-login-email">Email</label>
|
||||
<input
|
||||
id="cc-login-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="hb-form-row">
|
||||
<label htmlFor="cc-login-password">Password</label>
|
||||
<input
|
||||
id="cc-login-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="hb-error">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', marginTop: 8, minHeight: 48, fontSize: 15 }}
|
||||
>
|
||||
{loading ? 'Memproses…' : 'Masuk'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user