- Port halo_tokens + halo_theme + HaloButton to mitra_app (rose palette, Bricolage display, Poppins body, JetBrainsMono). - Build S3a Input WhatsApp (figma-bestie BestieS3 first half) with +62 chip, leading-zero/62 normalization, allow '+' in input. - Build S3b OTP verification (6-digit, 60s resend timer, attempts hint, Focus(canRequestFocus:false) for maestro inputText compat) with full error branching (CODE_MISMATCH, OTP_EXPIRED, OTP_USED, ATTEMPTS_EXCEEDED, WRONG_FLOW, ACCOUNT_INACTIVE). - Add AccountInactive terminal screen for is_active=false mitras. - Typed MitraAuthError with Indonesian-first localized messages + retryAfterSeconds passthrough. - Rebuild home_screen.dart to match figma BestieHome (greeting + status card + Ganti Status CTA + Pengingat + 2-tile dark grid). - Backend: POST /internal/_test/seed-mitra (idempotent) and PATCH /internal/mitras/:id (display_name update). - Control center: inline Edit Nama on mitras row + expandable inline log table under clicked mitra (vs old below-table panel). - 5 maestro flows ts-mitra-A-01/03/04/05/06 covering invalid input, happy path, account inactive, phone-format normalization, and the back-to-S3a regression. All green. Plan + memory documented in: - requirement/phase4-mitra-prehome-plan.md - requirement/flow_mitra.md / flow_mitra.mermaid.md §A Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Mitra App Flow — Mermaid Diagrams
Generated from
flow_mitra.mdand cross-checked against the Claude-Design handoff bundle inmitra_app/figma-bestie/project/screens/(HTML/JSX prototype — not part of the build).Scope:
- §A Pre-home auth (phone OTP + all retry/limit edge cases). No Claude-Design screens exist for this section yet — diagrams are spec-only and flag the missing screens the mitra_app needs to build.
- §1–§3 In-app flow once authenticated, mapped to the Bestie-* components in the Figma drop.
Screen ↔ design-component map
| Flow node | Design component | Source |
|---|---|---|
| Home — online standby | BestieHome (online=true) |
v4.jsx:417 |
| Home — offline | BestieHomeOffline (or BestieHome online=false) |
v5.jsx:188 · v4.jsx:417 |
| Bottom nav (Home / Chat / Profil) | BestieTabBar |
v4.jsx:464 |
| Undangan list — "Curhat Baru" tab | BestieInvites |
v4.jsx:480 |
| Undangan list — "Perpanjang Curhat" tab | BestieInvitesExtend |
v5.jsx:75 |
| Profil | BestieProfile |
v5.jsx:5 |
| Incoming popup — new curhat | BestieIncomingPopup (variant=new) |
v5.jsx:129 |
| Incoming popup — perpanjang | BestieIncomingPopup (variant=extend) |
v5.jsx:129 |
| Chat room — sesi aktif | BestieChatV5 (ended=false) · earlier draft BestieChat |
v5.jsx:222 · v4.jsx:523 |
| Chat room — durasi habis | BestieChatV5 (ended=true) |
v5.jsx:222 |
Design tokens & primitives (
HBOrb,HBButton, palettet) come from tokens.jsx and primitives.jsx — see project/CLAUDE.md before slicing.
A. Pre-Home — auth + OTP retry scenarios
Backend enforces four OTP limits in
otp.service.js; defaults fromconfig.service.js:215-218: verify_max_attempts=5, resend_cooldown=60s, max_per_phone=3/h, max_per_ip=10/h, OTP_TTL=5min. All four are tunable via the control center config.The current mitra app (login_screen.dart / otp_screen.dart) renders raw
error.toString()in a snackbar for every error — no resend button, no attempts-remaining hint, no blocked popup. Everything below marked 🆕 is missing UI the slicer needs to add.
A.1 Boot → login → OTP request
flowchart TD
Boot["App boot"] --> Token{"Refresh token<br/>valid?"}
Token -->|"yes"| Home["→ Home (skip auth)"]
Token -->|"no / expired"| Login["S3a · Input WhatsApp<br/><i>login_screen.dart</i>"]
Login -->|"Kirim OTP"| ReqApi["POST /api/mitra/auth/otp/request"]
ReqApi --> ReqOk{"Response"}
ReqOk -->|"200 ok"| OtpScreen["→ S3b · OTP verification"]
ReqOk -->|"422 PHONE_INVALID"| ErrPhone["🆕 Inline field error<br/>'Format nomor salah'"]
ErrPhone --> Login
ReqOk -->|"429 OTP_COOLDOWN"| ErrCool["🆕 Snackbar w/ countdown<br/>'Tunggu N detik' (retry_after_seconds)"]
ErrCool --> Login
ReqOk -->|"429 OTP_RATE_LIMIT_PHONE<br/>(3 reqs / hour)"| ErrPhoneLim["🆕 Popup · 'Terlalu banyak<br/>permintaan untuk nomor ini'<br/>+ retry-after timer"]
ErrPhoneLim --> Login
ReqOk -->|"429 OTP_RATE_LIMIT_IP<br/>(10 reqs / hour)"| ErrIpLim["🆕 Popup · 'Terlalu banyak<br/>permintaan dari jaringan ini'<br/>+ Hubungi admin CTA"]
ErrIpLim --> Login
classDef missing fill:#ffe5e5,stroke:#c44979
class ErrPhone,ErrCool,ErrPhoneLim,ErrIpLim missing
A.2 S3b verify — happy + every error path
flowchart TD
Otp["S3b · OTP verification (6-digit)<br/><i>otp_screen.dart</i><br/>🆕 + 'Kirim ulang kode' (60s cooldown)<br/>🆕 + 'Tersisa N percobaan' hint"]
Otp -->|"6 digits entered"| Verify["POST /api/mitra/auth/otp/verify"]
Verify --> Resp{"Response"}
Resp -->|"200 + is_active=true"| Home["→ Home<br/>(store tokens)"]
Resp -->|"422 CODE_INVALID"| BadFmt["🆕 Inline · 'Kode harus 6 digit'"]
BadFmt --> Otp
Resp -->|"401 CODE_MISMATCH<br/>(attempts < 5)"| Wrong["🆕 Clear fields · focus 1st<br/>'Kode salah · tersisa N percobaan'"]
Wrong --> Otp
Resp -->|"429 OTP_ATTEMPTS_EXCEEDED<br/>(5th wrong attempt)"| Blocked["🆕 Popup · 'Terlalu banyak percobaan'<br/>CTAs: Minta kode baru / Hubungi admin"]
Blocked -->|"Minta kode baru"| ResendNew["→ back to S3a (prefilled)"]
Blocked -->|"Hubungi admin"| Admin["External · WA / TG"]
Resp -->|"410 OTP_EXPIRED<br/>(> 5 min)"| Exp["🆕 Popup · 'Kode kadaluarsa'<br/>CTA: Minta kode baru"]
Exp --> ResendNew
Resp -->|"409 OTP_USED"| Used["🆕 Popup · 'Kode sudah dipakai'<br/>CTA: Minta kode baru"]
Used --> ResendNew
Resp -->|"400 WRONG_FLOW<br/>(non-mitra OTP)"| Wrong2["🆕 Popup · 'Bukan akun mitra'"]
Wrong2 --> ResendNew
Resp -->|"403 ACCOUNT_INACTIVE<br/>(code correct but mitra not approved)"| Inactive["🆕 Full-screen · 'Akun belum aktif'<br/>CTAs: WhatsApp admin / Telegram admin<br/>NO retry"]
classDef missing fill:#ffe5e5,stroke:#c44979
class Otp,BadFmt,Wrong,Blocked,Exp,Used,Wrong2,Inactive missing
A.3 Resend cooldown — local timer (on S3b)
flowchart TD
OtpView["S3b mounted"] --> Timer["🆕 Local 60s countdown<br/>starts on every request"]
Timer --> Btn{"Cooldown done?"}
Btn -->|"no"| Disabled["'Kirim ulang dalam Ns'<br/>button disabled"]
Disabled --> Timer
Btn -->|"yes"| Enabled["'Kirim ulang kode' enabled"]
Enabled -->|"tap"| ReReq["POST /api/mitra/auth/otp/request"]
ReReq -->|"200"| Reset["replace otp_request_id<br/>reset local attempts counter<br/>restart 60s timer"]
Reset --> Timer
ReReq -->|"429 OTP_COOLDOWN"| BackendCool["🆕 Snackbar w/ server retry_after<br/>(should never happen if local timer is right)"]
classDef missing fill:#ffe5e5,stroke:#c44979
class Timer,Disabled,Enabled,Reset,BackendCool missing
Implementation gaps (mitra_app)
| Screen / element | Where it lives today | What's missing |
|---|---|---|
| Inline phone-format error | login_screen.dart | All errors render as raw snackbar — needs field-level error + typed handler |
| Cooldown / rate-limit popups | login_screen.dart | No popup variants; retry_after_seconds is ignored |
| Resend button + 60s timer | otp_screen.dart | Code comment hints at it (line 72) but no UI |
| Attempts-remaining hint | otp_screen.dart | No local counter; user has no warning before the 5th attempt locks them out |
OTP_ATTEMPTS_EXCEEDED popup |
otp_screen.dart | Renders as plain snackbar; no CTA to recover |
OTP_EXPIRED / OTP_USED popup |
otp_screen.dart | Same — plain snackbar |
WRONG_FLOW popup |
otp_screen.dart | Same — plain snackbar |
ACCOUNT_INACTIVE screen |
otp_screen.dart | Renders as plain snackbar; should be a full-screen state with admin contact CTAs |
Design note: there's no Bestie-design equivalent for any of these screens (the figma-bestie drop starts at Home). Style with
t.brandSofter/t.dangerfrom tokens.jsx for visual continuity, and reuseHBButtonpatterns from the customer-side primitives.jsx until a mitra-specific design exists.
1. Home + availability gating
flowchart TD
Boot["App boot (post-auth)"] --> Status{"Mitra status?"}
Status -->|"offline"| HomeOff["Home · OFFLINE<br/><i>BestieHomeOffline</i>"]
Status -->|"online"| HomeOn["Home · standby (online)<br/><i>BestieHome online=true</i>"]
HomeOff -- "Ganti Status" --> HomeOn
HomeOn -- "Ganti Status" --> HomeOff
HomeOn --> Tabs["Bottom nav<br/><i>BestieTabBar</i>"]
HomeOff --> Tabs
Tabs -->|"Home"| HomeOn
Tabs -->|"Chat"| Undangan
Tabs -->|"Profil"| Profil
HomeOn -->|"tap Undangan tile"| Undangan["Undangan · Curhat Baru tab<br/><i>BestieInvites</i>"]
HomeOn -->|"tap Perpanjang tile"| UndanganExt["Undangan · Perpanjang tab<br/><i>BestieInvitesExtend</i>"]
HomeOn -->|"tap Profil"| Profil["Profil<br/><i>BestieProfile</i>"]
HomeOff -.->|"tiles disabled while offline"| HomeOff
2. Undangan list (tabbed) — accept / reject
flowchart TD
Entry["Home tile or Chat tab"] --> Tabs{"Which tab?"}
Tabs -->|"Curhat Baru"| InvNew["Undangan · Curhat Baru<br/><i>BestieInvites</i><br/>(new-client cards, brand pink)"]
Tabs -->|"Perpanjang Curhat"| InvExt["Undangan · Perpanjang<br/><i>BestieInvitesExtend</i><br/>(amber accent, +mins badge)"]
InvNew -->|"Tolak"| Home1["← back to Home"]
InvExt -->|"Tolak"| Home1
InvNew -->|"Terima"| ChatActive
InvExt -->|"Terima Perpanjangan"| ChatActive["Chat · sesi aktif<br/><i>BestieChatV5 ended=false</i>"]
Note: both Undangan variants share the same header + tab bar. The amber palette
+N mntbadge onBestieInvitesExtendis the visual cue separating extension invites from new-curhat invites.
3. Incoming request popup → chat → session end
flowchart TD
Idle["Mitra online (any screen)"] --> Push{"Incoming request"}
Push -->|"new curhat"| PopNew["Popup · Curhat Baru<br/><i>BestieIncomingPopup variant=new</i><br/>(brand pink, 30s window)"]
Push -->|"perpanjang"| PopExt["Popup · Perpanjang<br/><i>BestieIncomingPopup variant=extend</i><br/>(amber, 10s auto-accept)"]
PopNew -->|"Tolak"| Idle
PopExt -->|"Tolak"| Idle
PopNew -->|"Terima Sekarang"| Chat
PopExt -->|"Terima · +N mnt"| Chat
Chat["Chat · sesi aktif<br/><i>BestieChatV5 ended=false</i><br/>(SISA WAKTU pill, input bar)"]
Chat -->|"timer hits 00:00"| Ended["Chat · durasi habis<br/><i>BestieChatV5 ended=true</i><br/>(SELESAI pill, input replaced by notice)"]
Ended -->|"tunggu perpanjang"| PopExt
Ended -->|"tutup obrolan"| Idle
Open questions / gaps vs. design
- No design for the "no invitations" empty state of
BestieInvites/BestieInvitesExtend— both prototypes ship with 2 stub items. Confirm whether the empty state should reuse the homeUndangan: Belum adatile copy. - Offline + incoming: design only shows the popup on
Idle(online). Spec is silent on what happens if a request arrives while offline — likely suppressed by backend, but worth confirming so we don't render a dead popup. BestieOfflinePopup(v4.jsx:244) appears to be customer-side ("semua bestie lagi istirahat") — excluded from this mitra flow.- Reject animation/feedback: design returns to home with no toast. Confirm whether a "ditolak" snackbar is desired for parity with extension UX.