# Customer App Flow — Mermaid Diagrams

> Generated from `requirement/flow_customer.md` and cross-checked against the Figma
> handoff package (`requirement/Figma/` — git-ignored).
>
> **Legend (status pulled from a client_app audit, see `phase4-customer-flow.md`):**
> - 🟢 EXISTS — already shipped in client_app
> - 🟡 PARTIAL — close, but a key piece (UX, copy, or sub-state) is missing
> - 🔴 MISSING — no implementation in client_app yet

The flow is split into 6 sub-diagrams so each one stays readable. Screen IDs use
the Figma handoff naming (`S1`, `S6`, `S10`, …); see `Figma/handoff/png/` for the
909×540 renders and `Figma/screens/*.jsx` for the live source.

---

## 1. Boot + Home gating

```mermaid
flowchart TD
  S1["S1 · Splash 🟢"] --> Home{"JWT session?"}
  Home -->|"no"| Home1st["Home (1st time)<br/>+ login panel 🟢"]
  Home -->|"yes"| HomeRet["Home (returning)<br/>+ profile panel 🟢"]
  Home1st --> NotifCheck{"OS notif allowed?"}
  HomeRet --> NotifCheck
  NotifCheck -->|"no"| HomeBanner["Home + notif banner 🟢"]
  NotifCheck -->|"yes"| HomeReady["Home (ready) 🟢"]
  HomeBanner --> HomeReady

  HomeReady --> CTA{"CTA tapped?"}
  CTA -->|"'aku mau curhat'<br/>(1st time)"| NewUser["→ New User flow"]
  CTA -->|"'curhat sama bestie baru'<br/>(returning)"| ReturningUser["→ Returning flow"]

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
```

---

## 2. New-User onboarding (verified vs. anonymous)

```mermaid
flowchart TD
  Start["from Home — 'aku mau curhat' 🟢"] --> NameCheck{"call_sign exists?"}
  NameCheck -->|"no"| S2["S2 · Pengisian Nama 🟢"]
  NameCheck -->|"yes"| VerifChoice
  S2 --> VerifChoice["Verif vs Anon Choice Sheet<br/>(VerifChoiceSheet) 🟢"]

  VerifChoice -->|"verif WA · Rp2k"| USPGateA{"usp_seen flag? 🔴"}
  VerifChoice -->|"tanpa verif · Rp5k+"| USPGateB{"usp_seen flag? 🔴"}

  %% Verified path
  USPGateA -->|"no · first-timer"| USPa["S5b · USP screen 🟢"]
  USPGateA -->|"yes · skip"| S3a
  USPa --> S3a["S3a · WhatsApp input 🟢"]
  S3a --> S3b["S3b · OTP 4-digit 🟡"]
  S3b --> OTPok{"OTP ok?"}
  OTPok -->|"too many retries"| OTPBlock["OTP Blocked Popup 🟢<br/>→ fallback to Anon"]
  OTPBlock --> USPGateB
  OTPok -->|"verified"| UserLookup{"user found in DB?<br/>(phone match) 🔴"}
  UserLookup -->|"no · brand-new"| S6["S6 · Paywall Rp2.000<br/>(12 menit, sekali seumur hidup) 🟢"]
  UserLookup -->|"yes · existing account"| LoadCallSign["Load stored call_sign<br/>→ overwrite local call_sign 🔴"]
  LoadCallSign --> TransactedCheck{"has_transacted flag? 🔴"}
  TransactedCheck -->|"no · never paid"| S6
  TransactedCheck -->|"yes · returning verified"| PickMethod
  S6 --> Pay

  %% Anonymous path
  USPGateB -->|"no · first-timer"| USPb["S5b · USP screen 🟢"]
  USPGateB -->|"yes · skip"| PickMethod
  USPb --> PickMethod["Pilih cara curhat<br/>(chat / voice call) 🟢"]
  PickMethod --> PickDuration["Pemilihan harga<br/>(5 durations, full screen) 🟢"]
  PickDuration --> PayMethod["Cara bayar (QRIS-first) 🟢"]
  PayMethod --> Pay

  %% Shared payment exit
  Pay["Xendit checkout<br/>(QRIS / e-wallet) 🟡"] --> WaitPay["Waiting Payment<br/>(20-min QRIS clock) 🟢"]
  WaitPay --> PayStat{"payment status"}
  PayStat -->|"timeout 20 min"| PayExpired["Pembayaran expired 🟢<br/>→ retry"]
  PayExpired --> Pay
  PayStat -->|"paid"| NotifGate

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
  class UserLookup,LoadCallSign,TransactedCheck,USPGateA,USPGateB missing
  class S3b,Pay partial
```

> **ESP screening removed (2026-05-12):** the S5 ESP multi-select chip
> screen is retired from the spec. Both verified and anonymous branches
> now go from `VerifChoiceSheet` straight to the `usp_seen?` gate. Any
> ESP code still in `client_app` is now tech debt to retire — see
> `TECH_DEBT.md`.
>
> **S5b USP is one-time-only (2026-05-12):** the USP screen now shows at
> most once per user. Gating is driven by a `usp_seen` flag that lives in
> two places:
> - **Local (SharedPreferences):** the runtime gate. Set to `true` when the
>   user dismisses the USP screen for the first time. Survives across
>   sessions on the same device.
> - **DB (`customers.usp_seen` column 🔴):** the cross-device source of truth.
>   Written when the user account is created (post-OTP, after JWT issuance
>   and `users` row insert) if local flag is already true, OR on any
>   subsequent USP dismissal once an account exists. Read on login/relogin
>   and hydrated back into local. "True wins" — if either side says seen,
>   the gate is closed.
>
> Anonymous users with no account only have the local flag; if they later
> upgrade to verified, account creation propagates the local flag to DB.
> Returning verified users on a fresh device will see USP at most once on
> that device (DB hydrate happens on login, after USP gate fires pre-OTP).
> Business has accepted this edge case.
>
> **Anchor mismatch:** flow_customer.md numbers ESP/USP under
> `5.1.2 Verification request (OTP)` for both branches, but Figma puts
> `VerifChoiceSheet` *before* ESP. The mermaid above follows Figma; reconcile in
> phase4 spec.
>
> **Post-OTP account lookup (added 2026-05-11):** the verified path is not
> always "new user". After a successful OTP, the backend looks up the phone
> number; if a row exists, the app overwrites the freshly-typed call_sign with
> the stored one. Then `has_transacted` decides routing:
> - `false` → S6 Paywall (Rp2.000 first-session) — user has an account but
>   never converted.
> - `true` → jump straight into `PickMethod` (the regular chat/voice +
>   duration + payment flow). USPb is already skipped because USPGateA
>   already evaluated pre-OTP on the verified branch.
>
> `has_transacted` is the persistent flag on the users table that flips the
> first-time pricing off forever. Backend phone-lookup behaviour already
> exists (see Phase 1 auto-link via phone); the app-side reconciliation +
> `has_transacted` plumbing is the new work.

---

## 3. Pre-pairing → Searching → Match (shared)

```mermaid
flowchart TD
  NotifGate["Notif Gate Screen 🟢<br/>(Aktifkan / Nanti Saja)"] --> NotifBranch{"OS allowed?"}
  NotifBranch -->|"no + ask"| EnableNotif["OS settings deeplink"] --> SoftPrompt
  NotifBranch -->|"yes / skipped"| SoftPrompt
  SoftPrompt["S7 · Soft-prompt<br/>(consent + warmup, CTA 'Aku ngerti, Lanjut') 🟡"] --> Blast
  Blast["Blast pair request<br/>S7 · Searching state 🟢"] --> BlastTimer{"5-min timer"}
  BlastTimer -->|"matched"| S9["S9 · Match Found<br/>(bestie name, age, hobi) 🟡"]
  BlastTimer -->|"timeout"| S7Timeout["S7 · Timeout 5 menit 🟢<br/>CTA 'Coba Cari Lagi'<br/>ghost CTA 'Coba cari lagi nanti' → Home"]
  S7Timeout -->|"retry"| Blast
  S7Timeout -->|"home"| HomeRet
  S9 --> S10
  S10["S10 · Chat Room"]
  HomeRet["→ Home (returning)"]

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
  class SoftPrompt,S9 partial
```

---

## 4. Returning-User pairing (lama / baru)

```mermaid
flowchart TD
  CTA["'curhat sama bestie baru' 🟢"] --> Choice["Bestie Choice Sheet<br/>(BestieChoiceSheet) 🟢"]
  Choice -->|"bestie yang udah kenal"| HistList["Bestie History List<br/>(BestieHistoryList) 🟢"]
  Choice -->|"bestie baru"| BlastFlow["→ S7 Soft-prompt + Blast<br/>(see diagram 3)"]

  HistList --> PickBestie["pick bestie"]
  PickBestie --> CheckOnline{"bestie online?"}
  CheckOnline -->|"no"| OfflinePopup["Bestie Offline Popup<br/>(returning variant) 🟢"]
  OfflinePopup -->|"cari bestie lain"| BlastFlow
  OfflinePopup -->|"tanya admin"| AdminSheet["Sheet · tanya admin<br/>(WA / Telegram) 🟢"]
  CheckOnline -->|"yes"| Targeted["Request targeted pair<br/>'Menunggu bestie tertentu' 🟢<br/>(20s countdown overlay)"]
  Targeted --> TargetedRes{"mitra answers?"}
  TargetedRes -->|"accept"| S10["→ S10 Chat Room"]
  TargetedRes -->|"reject / timeout"| OfflinePopup

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
```

---

## 5. Chat Room (S10) — countdown UX

```mermaid
flowchart TD
  Enter["enter S10 · Chat Room<br/>(WebSocket open, timer running) 🟢"] --> T3{"3-minutes-left tick"}
  T3 -->|"fired"| Snackbar["S10 · Snackbar reminder<br/>'sisa 3 menit lagi ya' 🟢"]
  Snackbar --> T2
  T3 -->|"not yet"| T2{"2-minutes-left tick"}
  T2 -->|"fired"| LowTime["S10 · Last 2 Minutes<br/>(timer turns danger color) 🟢"]
  LowTime --> Expire
  T2 -->|"not yet"| Expire{"timer hits 0"}
  Expire -->|"fired"| ExpiredBanner["S10 · Floating Expired Banner<br/>'habis nih... mau lanjutin?' 🟢"]
  ExpiredBanner --> CTAExt{"perpanjang CTA?"}
  CTAExt -->|"yes"| TimeUp
  CTAExt -->|"close / ignore"| EndFlow["→ end-session flow"]

  Expire -->|"not yet · user taps perpanjang"| TimeUp
  TimeUp["Time-up Bottom Sheet<br/>(5 durations · chat/call toggle) 🟢"]
  TimeUp -->|"perpanjang"| AskMitra["Targeted re-pay request<br/>(same mitra, no blast) 🟢"]
  TimeUp -->|"cukup, akhiri sesi"| EndFlow

  AskMitra --> MitraRes{"mitra approves?"}
  MitraRes -->|"yes + paid"| Enter
  MitraRes -->|"reject"| OfflinePopup["Bestie Offline Popup<br/>(returning variant) 🟢"]

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
```

---

## 6. End-of-session sequence (2-step confirm + closing message)

```mermaid
flowchart TD
  EndStart["End-session entry<br/>(from S10 or Time-up sheet 'Cukup, Akhiri')"] --> Confirm1["Popup · Konfirmasi Akhiri (1)<br/>'beneran udah cukup?' 🟢"]
  Confirm1 -->|"Gak Jadi, Balik"| TimeUp["Time-up Bottom Sheet 🟢"]
  Confirm1 -->|"Lanjut Akhiri"| Confirm2["Popup · Konfirmasi Akhiri (2)<br/>'mau tinggalin pesan penutup?' 🟢"]

  Confirm2 -->|"Tulis Pesan Penutup"| ClosingSheet["Pesan Penutup Bottom Sheet<br/>(textarea) 🟢"]
  Confirm2 -->|"Lewati Saja"| ThankYou

  ClosingSheet -->|"Kirim & Akhiri"| MitraReceipt{"mitra rejects close?"}
  ClosingSheet -->|"Lewat — Langsung Akhiri"| ThankYou
  MitraReceipt -->|"no"| ThankYou
  MitraReceipt -->|"yes (rare)"| OfflinePopup["Bestie Offline Popup 🟢"]

  ThankYou["S11 · Terima Kasih Udah Cerita 🟢"] --> Home["→ Home (returning) 🟢"]

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
```

---

## 7. Chat Tab (3 sub-tabs) — Phase 4 Stage 10

```mermaid
flowchart TD
  HomeTabBar["HaloTabBar · tap '💬 chat'"] --> ChatRedirect["/chat redirect 🟡"]
  ChatRedirect --> Aktif["/chat/aktif · sub-tab 🟡"]

  Aktif -->|"tap pill 'pembayaran'"| Pembayaran["/chat/pembayaran · sub-tab 🟡"]
  Aktif -->|"tap pill 'selesai'"| Selesai["/chat/selesai · sub-tab 🟡"]
  Pembayaran -->|"tap pill 'aktif'"| Aktif
  Pembayaran -->|"tap pill 'selesai'"| Selesai
  Selesai -->|"tap pill 'aktif'"| Aktif
  Selesai -->|"tap pill 'pembayaran'"| Pembayaran

  Aktif -->|"empty"| AktifEmpty["empty state · 'belum ada chat di sini' 🟡"]
  Pembayaran -->|"empty"| PembayaranEmpty["empty state · 'belum ada pembayaran tertunda' 🟡"]
  Selesai -->|"empty"| SelesaiEmpty["empty state · 'belum ada riwayat curhat' 🟡"]

  Aktif -->|"tap active session row"| ChatRoom["S10 · Chat Room 🟢"]
  Pembayaran -->|"tap pending payment row"| WaitingPayment["S7 · Waiting Payment 🟢"]
  Selesai -->|"tap past session row"| Transcript["Read-only transcript /chat/transcript/:id 🟢"]

  classDef missing fill:#ffe5e5,stroke:#c44979
  classDef partial fill:#fff4d6,stroke:#c69b3f
  class ChatRedirect,Aktif,Pembayaran,Selesai,AktifEmpty,PembayaranEmpty,SelesaiEmpty partial
```

**Data sources**
- Aktif → existing `GET /api/client/chat/session/active-with-unread` (0 or 1 row)
- Pembayaran → new `GET /api/client/payment-sessions/pending` (filters `status='pending' AND expires_at > NOW()`)
- Selesai → existing `GET /api/client/history`, migrated to cursor pagination (`{ items, next_cursor, has_more }`)

**Badges**
- Bottom-nav `chat` tab → red dot when Pembayaran `total > 0`
- `aktif` sub-tab pill → numeric badge from `unread_count`
- `pembayaran` sub-tab pill → numeric badge from `total`
- `selesai` sub-tab pill → no badge

---

## Cross-reference: Figma → flow_customer.md

| Figma artifact (file) | Source | flow_customer.md ref |
|---|---|---|
| S1 Splash | `screens/onboarding.jsx::S1Splash` | §1 |
| Home 1st / Returning | `screens/v3.jsx::SHome1st` / `SHomeReturning` + `screens/session.jsx::S12Home` | §2–3 |
| Notif banner on home | `screens/v3.jsx::HBNotifBanner` | §4.1 |
| S2 Nama | `screens/onboarding.jsx::S2Name` (and v4 variant) | §5.1.1 |
| `VerifChoiceSheet` | `screens/v4.jsx::VerifChoiceSheet` | implied between §5.1.1 ↔ §5.1.2 |
| ~~S5 ESP screening~~ | ~~`screens/onboarding.jsx::S5ESP`~~ | **retired 2026-05-12** — code is tech debt |
| S5b USP (one-time) | `screens/onboarding.jsx::S5USP` | §5.1.2.1.2 / §5.1.2.2.2 — gated by `usp_seen` |
| S3a WA / S3b OTP | `screens/onboarding.jsx::S3Phone` (+ `screens/v4.jsx::S3OTPV4`) | §5.1.2.1.3-4 |
| `OTPBlockedPopup` | `screens/v4.jsx::OTPBlockedPopup` | (gap — not in flow doc) |
| S6 Paywall Rp2k | `screens/onboarding.jsx::S6Paywall` | §5.1.2.1.5 |
| Pilih cara curhat | `screens/v3.jsx::SPickMethod` | §5.1.2.2.3 |
| Pemilihan harga | `screens/v3.jsx::SPickDuration` (+ `v4::InitialDurationPicker`) | §5.1.2.2.4 |
| Cara bayar | `screens/extras.jsx::SPaymentMethod` | §5.1.2.2.5 |
| Waiting Payment | `screens/extras.jsx::SWaitingPayment` | §5.1.4 |
| Pembayaran expired | `screens/v4.jsx::PaymentExpiredV4` (+ extras `SWaitingPayment expired`) | §5.1.4.1 |
| Notif Gate (full screen) | `screens/extras.jsx::SNotifGate` (+ `v4::NotifGateV4`) | §5.1.5.1 |
| S7 Soft-prompt + Searching | `screens/session.jsx::S7Prompt` + `S8Searching` + `screens/v3.jsx::SSearchPrompt` | §5.1.6-8 |
| S7 Timeout 5 menit | `screens/v3.jsx::SSearchPrompt(state='timeout')` | §5.1.8.2 |
| S9 Match | `screens/session.jsx::S9Match` (+ `v4::S9MatchV4`) | §5.1.9 |
| Bestie Choice Sheet | `screens/v4.jsx::BestieChoiceSheet` | §5.2.1 |
| Bestie History List | `screens/v4.jsx::BestieHistoryList` | §5.2.1.1 |
| Bestie Offline Popup | `screens/v4.jsx::BestieOfflinePopup` | §5.2.1.1.1 / §5.7.8.1.1.1 |
| Tanya Admin Sheet | `screens/v3.jsx::HBContactAdminSheet` | §5.2.1.1.1.2 |
| Menunggu Bestie | `screens/extras.jsx::SWaitingBestie` | §5.2.1.1.2.1 |
| S10 Chat | `screens/session.jsx::S10Chat` | §5.3 |
| 3-min Snackbar reminder | `screens/v3.jsx::HBSnackbar` (configured for "sisa 3 menit") | §5.4 |
| Last-2-min visuals | `screens/session.jsx::S10Chat` (lowTime branch) | §5.5 |
| Floating expired banner | `screens/v3.jsx::HBChatExpiredBanner` | §5.6 |
| Time-up Bottom Sheet | `screens/extras.jsx::STimeUpSheet` | §5.7-8 |
| Confirm akhiri (2 popups) | `screens/v3.jsx::HBConfirmEndPopup` (step 1 + 2) | §5.8.2.1 / §5.8.2.1.1 |
| Closing Message Sheet | `screens/extras.jsx::SClosingSheet` | §5.8.2.1.1.1 |
| S11 Thank-you | `screens/session.jsx::S11Post` | §5.8.2.1.1.1.1 |
| Chat Tab (3 sub-tabs) | `screens/extras.jsx::SChatList` | §7 |

Anything Figma describes that flow_customer.md doesn't mention is captured as a
gap in `phase4-customer-flow.md` (next-phase doc).
