Files
halobestie-clone/requirement/flow_mitra.mermaid.md
Ramadhan Sjamsani 9696eadeaf Mitra §A: pre-home (S3a/S3b/AccountInactive) + design system + Bestie Home
- 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>
2026-05-19 22:01:28 +08:00

12 KiB
Raw Blame History

Mitra App Flow — Mermaid Diagrams

Generated from flow_mitra.md and cross-checked against the Claude-Design handoff bundle in mitra_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, palette t) 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 from config.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 &lt; 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/>(&gt; 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.danger from tokens.jsx for visual continuity, and reuse HBButton patterns 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 mnt badge on BestieInvitesExtend is 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 home Undangan: Belum ada tile 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.