# 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.
