# Mitra App Flow — Mermaid Diagrams > Generated from [`flow_mitra.md`](flow_mitra.md) and cross-checked against the > Claude-Design handoff bundle in > [`mitra_app/figma-bestie/project/screens/`](../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](../mitra_app/figma-bestie/project/screens/v4.jsx#L417) | | Home — offline | `BestieHomeOffline` (or `BestieHome` online=false) | [v5.jsx:188](../mitra_app/figma-bestie/project/screens/v5.jsx#L188) · [v4.jsx:417](../mitra_app/figma-bestie/project/screens/v4.jsx#L417) | | Bottom nav (Home / Chat / Profil) | `BestieTabBar` | [v4.jsx:464](../mitra_app/figma-bestie/project/screens/v4.jsx#L464) | | Undangan list — "Curhat Baru" tab | `BestieInvites` | [v4.jsx:480](../mitra_app/figma-bestie/project/screens/v4.jsx#L480) | | Undangan list — "Perpanjang Curhat" tab | `BestieInvitesExtend` | [v5.jsx:75](../mitra_app/figma-bestie/project/screens/v5.jsx#L75) | | Profil | `BestieProfile` | [v5.jsx:5](../mitra_app/figma-bestie/project/screens/v5.jsx#L5) | | Incoming popup — new curhat | `BestieIncomingPopup` (variant=`new`) | [v5.jsx:129](../mitra_app/figma-bestie/project/screens/v5.jsx#L129) | | Incoming popup — perpanjang | `BestieIncomingPopup` (variant=`extend`) | [v5.jsx:129](../mitra_app/figma-bestie/project/screens/v5.jsx#L129) | | Chat room — sesi aktif | `BestieChatV5` (ended=false) · earlier draft `BestieChat` | [v5.jsx:222](../mitra_app/figma-bestie/project/screens/v5.jsx#L222) · [v4.jsx:523](../mitra_app/figma-bestie/project/screens/v4.jsx#L523) | | Chat room — durasi habis | `BestieChatV5` (ended=true) | [v5.jsx:222](../mitra_app/figma-bestie/project/screens/v5.jsx#L222) | > Design tokens & primitives (`HBOrb`, `HBButton`, palette `t`) come from > [tokens.jsx](../mitra_app/figma-bestie/project/screens/tokens.jsx) and > [primitives.jsx](../mitra_app/figma-bestie/project/screens/primitives.jsx) — see > [project/CLAUDE.md](../mitra_app/figma-bestie/project/CLAUDE.md) before slicing. --- ## A. Pre-Home — auth + OTP retry scenarios > Backend enforces four OTP limits in > [`otp.service.js`](../backend/src/services/otp.service.js); defaults from > [`config.service.js:215-218`](../backend/src/services/config.service.js#L215-L218): > 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](../mitra_app/lib/features/auth/screens/login_screen.dart) / > [otp_screen.dart](../mitra_app/lib/features/auth/screens/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 ```mermaid flowchart TD Boot["App boot"] --> Token{"Refresh token
valid?"} Token -->|"yes"| Home["→ Home (skip auth)"] Token -->|"no / expired"| Login["S3a · Input WhatsApp
login_screen.dart"] 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
'Format nomor salah'"] ErrPhone --> Login ReqOk -->|"429 OTP_COOLDOWN"| ErrCool["🆕 Snackbar w/ countdown
'Tunggu N detik' (retry_after_seconds)"] ErrCool --> Login ReqOk -->|"429 OTP_RATE_LIMIT_PHONE
(3 reqs / hour)"| ErrPhoneLim["🆕 Popup · 'Terlalu banyak
permintaan untuk nomor ini'
+ retry-after timer"] ErrPhoneLim --> Login ReqOk -->|"429 OTP_RATE_LIMIT_IP
(10 reqs / hour)"| ErrIpLim["🆕 Popup · 'Terlalu banyak
permintaan dari jaringan ini'
+ 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 ```mermaid flowchart TD Otp["S3b · OTP verification (6-digit)
otp_screen.dart
🆕 + 'Kirim ulang kode' (60s cooldown)
🆕 + '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
(store tokens)"] Resp -->|"422 CODE_INVALID"| BadFmt["🆕 Inline · 'Kode harus 6 digit'"] BadFmt --> Otp Resp -->|"401 CODE_MISMATCH
(attempts < 5)"| Wrong["🆕 Clear fields · focus 1st
'Kode salah · tersisa N percobaan'"] Wrong --> Otp Resp -->|"429 OTP_ATTEMPTS_EXCEEDED
(5th wrong attempt)"| Blocked["🆕 Popup · 'Terlalu banyak percobaan'
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
(> 5 min)"| Exp["🆕 Popup · 'Kode kadaluarsa'
CTA: Minta kode baru"] Exp --> ResendNew Resp -->|"409 OTP_USED"| Used["🆕 Popup · 'Kode sudah dipakai'
CTA: Minta kode baru"] Used --> ResendNew Resp -->|"400 WRONG_FLOW
(non-mitra OTP)"| Wrong2["🆕 Popup · 'Bukan akun mitra'"] Wrong2 --> ResendNew Resp -->|"403 ACCOUNT_INACTIVE
(code correct but mitra not approved)"| Inactive["🆕 Full-screen · 'Akun belum aktif'
CTAs: WhatsApp admin / Telegram admin
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) ```mermaid flowchart TD OtpView["S3b mounted"] --> Timer["🆕 Local 60s countdown
starts on every request"] Timer --> Btn{"Cooldown done?"} Btn -->|"no"| Disabled["'Kirim ulang dalam Ns'
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
reset local attempts counter
restart 60s timer"] Reset --> Timer ReReq -->|"429 OTP_COOLDOWN"| BackendCool["🆕 Snackbar w/ server retry_after
(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](../mitra_app/lib/features/auth/screens/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](../mitra_app/lib/features/auth/screens/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](../mitra_app/figma-bestie/project/screens/tokens.jsx) for > visual continuity, and reuse `HBButton` patterns from the customer-side > [primitives.jsx](../mitra_app/figma-bestie/project/screens/primitives.jsx) > until a mitra-specific design exists. --- ## 1. Home + availability gating ```mermaid flowchart TD Boot["App boot (post-auth)"] --> Status{"Mitra status?"} Status -->|"offline"| HomeOff["Home · OFFLINE
BestieHomeOffline"] Status -->|"online"| HomeOn["Home · standby (online)
BestieHome online=true"] HomeOff -- "Ganti Status" --> HomeOn HomeOn -- "Ganti Status" --> HomeOff HomeOn --> Tabs["Bottom nav
BestieTabBar"] HomeOff --> Tabs Tabs -->|"Home"| HomeOn Tabs -->|"Chat"| Undangan Tabs -->|"Profil"| Profil HomeOn -->|"tap Undangan tile"| Undangan["Undangan · Curhat Baru tab
BestieInvites"] HomeOn -->|"tap Perpanjang tile"| UndanganExt["Undangan · Perpanjang tab
BestieInvitesExtend"] HomeOn -->|"tap Profil"| Profil["Profil
BestieProfile"] HomeOff -.->|"tiles disabled while offline"| HomeOff ``` --- ## 2. Undangan list (tabbed) — accept / reject ```mermaid flowchart TD Entry["Home tile or Chat tab"] --> Tabs{"Which tab?"} Tabs -->|"Curhat Baru"| InvNew["Undangan · Curhat Baru
BestieInvites
(new-client cards, brand pink)"] Tabs -->|"Perpanjang Curhat"| InvExt["Undangan · Perpanjang
BestieInvitesExtend
(amber accent, +mins badge)"] InvNew -->|"Tolak"| Home1["← back to Home"] InvExt -->|"Tolak"| Home1 InvNew -->|"Terima"| ChatActive InvExt -->|"Terima Perpanjangan"| ChatActive["Chat · sesi aktif
BestieChatV5 ended=false"] ``` > 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 ```mermaid flowchart TD Idle["Mitra online (any screen)"] --> Push{"Incoming request"} Push -->|"new curhat"| PopNew["Popup · Curhat Baru
BestieIncomingPopup variant=new
(brand pink, 30s window)"] Push -->|"perpanjang"| PopExt["Popup · Perpanjang
BestieIncomingPopup variant=extend
(amber, 10s auto-accept)"] PopNew -->|"Tolak"| Idle PopExt -->|"Tolak"| Idle PopNew -->|"Terima Sekarang"| Chat PopExt -->|"Terima · +N mnt"| Chat Chat["Chat · sesi aktif
BestieChatV5 ended=false
(SISA WAKTU pill, input bar)"] Chat -->|"timer hits 00:00"| Ended["Chat · durasi habis
BestieChatV5 ended=true
(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](../mitra_app/figma-bestie/project/screens/v4.jsx#L244)) 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.