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

232 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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
```mermaid
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)
```mermaid
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](../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<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
```mermaid
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
```mermaid
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](../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.