- 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>
232 lines
12 KiB
Markdown
232 lines
12 KiB
Markdown
# 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 < 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)
|
||
|
||
```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.
|