Phase 4 PRD + plan: customer-flow redesign (Figma alignment)
Adds the Phase 4 requirement docs that align the customer app with the new HaloBestie Figma design dump. - requirement/flow_customer.md: source-of-truth numbered flow (input) - requirement/flow_customer.mermaid.md: 6 mermaid diagrams + Figma cross-ref - requirement/phase4-customer-flow.md: PRD (15 functional sections) - requirement/phase4-customer-flow-plan.md: 10-stage implementation plan - .gitignore: exclude requirement/Figma.zip + extracted Figma/ folder Resolved product decisions: no free trial (replaced by configurable first-session discount), pricing has independent chat/call groups, voice-call mode is chat-with-badge (mitra shares Meet link manually), social login is server-driven via /api/shared/auth-providers, ESP tags are info-only (not used for matching). No code changes; implementation starts at plan stage 0 (design system). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
601
requirement/phase4-customer-flow.md
Normal file
601
requirement/phase4-customer-flow.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# PRD: Phase 4 — Customer Flow Redesign (Figma Alignment)
|
||||
|
||||
> Source-of-truth flow: [`flow_customer.md`](flow_customer.md) (numbered list) +
|
||||
> [`flow_customer.mermaid.md`](flow_customer.mermaid.md) (cross-referenced
|
||||
> diagrams). Visual design dump in `requirement/Figma/` (git-ignored).
|
||||
|
||||
# Overview
|
||||
|
||||
**Goal:** Bring the customer app in line with the new HaloBestie design package
|
||||
shipped in `Figma.zip`. Phase 1–3.7 already cover auth, pairing, chat, and the
|
||||
pay-before-blast loop, but the new design adds:
|
||||
|
||||
- a **verified-vs-anonymous decision sheet** at onboarding,
|
||||
- **ESP screening** (multi-select emotional-state chips) and a dedicated **USP screen** before paywall,
|
||||
- a **chat / voice-call mode toggle** with separate pricing,
|
||||
- an explicit **QRIS-first payment screen** + 20-min waiting/expiry pages,
|
||||
- a **notification gate** screen + home banner variant,
|
||||
- the **soft-prompt + 5-min searching timeout** UX,
|
||||
- chat-room **time warnings** (3-min snackbar, last-2-min danger visuals, expired floating banner),
|
||||
- a **2-step "akhiri sesi" confirm** + dedicated **closing-message bottom sheet**,
|
||||
- a proper **S11 thank-you** screen,
|
||||
- the **bestie-choice / bestie-history / targeted-wait** flow on returning users (the targeted-wait countdown overlay specifically),
|
||||
- secondary safety nets: **OTP-blocked → fallback to anon**, **tanya admin** (WA/Telegram) sheet.
|
||||
|
||||
**Out of scope (defer):**
|
||||
- Real Xendit integration — payment provider stays mocked through this phase
|
||||
(per memory `Pricing Still Mocked Through Phase 3.7`); only stub the *UI* for
|
||||
QRIS / payment-method / 20-min wait / expired so it's wired once payments go
|
||||
real. **Pricing values themselves become configurable** via CC in this phase
|
||||
(see §3 + §3.5).
|
||||
- Voice/video media session — voice-call mode in Phase 4 is **just a chat
|
||||
session with a different price group + a "Voice Call" badge in the chat
|
||||
header**. The mitra manually shares a Google Meet (or equivalent) link in
|
||||
the chat as a normal message; we do not validate, generate, or proxy the
|
||||
link. Real call media handling is a later phase.
|
||||
- Mitra-side composer helpers for Google Meet links (no "insert meet link"
|
||||
button or template) — left to ops convention; revisit if it becomes a
|
||||
friction point.
|
||||
- Mitra-side changes from Figma's `BestieHome / BestieInvites / BestieChat`
|
||||
mockups — those overlap with the existing mitra_app and don't change customer
|
||||
UX in this phase.
|
||||
|
||||
**Affects:** `client_app` (heavy), `backend` (additive endpoints), `control_center` (light: pricing-tier readonly view in case mock changes).
|
||||
|
||||
---
|
||||
|
||||
# Background & Audit
|
||||
|
||||
The audit (see `flow_customer.mermaid.md` legend) classified every screen in the
|
||||
new flow as 🟢 EXISTS, 🟡 PARTIAL, or 🔴 MISSING. Numbers from the audit:
|
||||
|
||||
| Group | EXISTS | PARTIAL | MISSING |
|
||||
|---|---:|---:|---:|
|
||||
| Onboarding & Auth | 4 | 3 | 2 |
|
||||
| Payment | 1 | 1 | 4 |
|
||||
| Pairing & Match | 2 | 3 | 2 |
|
||||
| Chat session | 1 | 2 | 3 |
|
||||
| Session-end | 0 | 2 | 2 |
|
||||
| Returning user | 1 | 1 | 3 |
|
||||
|
||||
**This phase resolves all 🟡 PARTIAL and 🔴 MISSING items. EXISTS screens are
|
||||
only touched for visual alignment with the Figma palette + tokens.**
|
||||
|
||||
Screen-by-screen detail (and the file path of the closest existing
|
||||
implementation, where applicable) lives in `flow_customer.mermaid.md` Section
|
||||
"Cross-reference: Figma → flow_customer.md" plus the audit at the bottom of
|
||||
this doc.
|
||||
|
||||
---
|
||||
|
||||
# Functional Requirements
|
||||
|
||||
## 1. Verif vs. Anon decision (sheet)
|
||||
|
||||
> Figma: `screens/v4.jsx::VerifChoiceSheet` · flow §5.1 (implied between Nama
|
||||
> and Verif/Anon branches).
|
||||
|
||||
### 1.1
|
||||
- After **S2 Nama** finishes, show a bottom sheet with two CTAs:
|
||||
- **"verif WA · Rp2.000"** → routes to ESP → USP → S3a WA input → S3b OTP → S6 Paywall → QRIS payment.
|
||||
- **"tanpa verif · mulai Rp5.000"** → routes to ESP → USP → Pilih cara curhat → Pemilihan harga → Cara bayar.
|
||||
- Sheet is **dismissable** with no default — back-tap returns to S2 Nama (does not auto-pick).
|
||||
- This sheet is shown **once per fresh install before first session**; for users who already paid Rp2k once, the sheet is skipped and we go straight to "Pemilihan harga" (returning-user economics).
|
||||
|
||||
### 1.2 Backend
|
||||
- New: `GET /api/client/onboarding-state` → returns `{ has_paid_first_session: bool }`. Used by client_app to pick whether to show this sheet.
|
||||
|
||||
## 2. ESP screening + USP screen
|
||||
|
||||
> Figma: `screens/onboarding.jsx::S5ESP` and `S5USP`. Existing
|
||||
> `topic_selection_bottom_sheet.dart` is binary-only and is replaced by ESP.
|
||||
|
||||
### 2.1 ESP (Emotional State Picker)
|
||||
- 12 chips (`hubungan, keluarga, kerja/sekolah, diri sendiri, cemas, sedih, kesepian, bingung arah, kesel, capek banget, pengen ngomong, lainnya`) — multi-select, ≥1 required to continue, **skip CTA** allowed.
|
||||
- Result is sent to backend as `topics: string[]` and **stored on the chat
|
||||
session for informational purposes only** — surfaced to the mitra at session
|
||||
start, surfaced in CC for analytics. **Not used for matching.** No mapping
|
||||
layer, no routing change. Topic-aware matching, if ever needed, is a later
|
||||
phase.
|
||||
|
||||
### 2.2 USP screen
|
||||
- Static, four-bullet trust hook: "manusia nyata", "hampir instan", "hampir 24 jam", "kayak sahabat". CTA "aku ngerti, lanjut →".
|
||||
- Route step-dot is `current=2` of 4 (matches `screens/onboarding.jsx::S5USP`).
|
||||
|
||||
## 3. Pricing & duration picker (two independent groups)
|
||||
|
||||
> Figma: `screens/v3.jsx::SPickDuration` + `screens/v4.jsx::InitialDurationPicker`.
|
||||
|
||||
### 3.1 Two price groups
|
||||
|
||||
Pricing has **two independent tier lists**, one per mode:
|
||||
|
||||
- **Chat** — text-based session. Default tiers: `5/10/30/60/120 menit`.
|
||||
- **Voice call** — same WebSocket chat, but mitra is expected to share a
|
||||
Google Meet link manually. Default tiers: `10/20/45/60 menit` at
|
||||
premium prices.
|
||||
|
||||
Both lists are **fully configurable** via CC (no hard-coded multiplier). Each
|
||||
tier carries: `id`, `minutes`, `price_idr`, `tag` (optional, e.g. `paling pas`,
|
||||
`hemat`, `best deal`).
|
||||
|
||||
### 3.2 Mode toggle
|
||||
- Pill toggle at top of the duration picker: `chat | call`. Defaults to
|
||||
`chat`.
|
||||
- Toggling rebuilds the option list from the corresponding tier group.
|
||||
- For **first-session-discount-eligible users** (see §3.5), the call toggle is
|
||||
**hidden** unless ops enables call for first session via
|
||||
`first_session_discount_modes` config (default: `["chat"]` only).
|
||||
|
||||
### 3.3 Picker UX
|
||||
- Each option renders price + tag. Selection persists across back-nav.
|
||||
- Bottom CTA reads `bayar Rp{price}` with the mode icon (`💬` or `📞`).
|
||||
|
||||
## 3.5 First-session discount (replaces "free trial")
|
||||
|
||||
> Figma: `screens/onboarding.jsx::S6Paywall` (the "Rp2.000 untuk 12 menit"
|
||||
> screen). **There is no free trial in Phase 4** — the previous free-trial
|
||||
> concept is replaced with a configurable first-session discount.
|
||||
|
||||
### 3.5.1 Eligibility
|
||||
- User is **OTP-verified** (came through the verified onboarding branch), **and**
|
||||
- User has **no prior consumed session** (no `chat_sessions` row with terminal
|
||||
success state for this `customer_id`).
|
||||
|
||||
Anonymous users never see the discount; verified users who already consulted
|
||||
once also do not. Eligibility is determined by the backend at the point of
|
||||
displaying the paywall — never trust client.
|
||||
|
||||
### 3.5.2 Configurable values (CC)
|
||||
All four of these live in `app_config`, editable from CC:
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `first_session_discount_enabled` | `true` | Master kill-switch |
|
||||
| `first_session_discount_actual_price_idr` | `2000` | The user pays this |
|
||||
| `first_session_discount_gimmick_price_idr` | `12000` | Struck-through "before" price |
|
||||
| `first_session_discount_duration_minutes` | `12` | Session length |
|
||||
| `first_session_discount_modes` | `["chat"]` | Which modes the discount applies to (chat / call). For Phase 4 default chat-only. |
|
||||
|
||||
### 3.5.3 UX
|
||||
- After Verif Choice → ESP → USP, eligible users land on the **S6 paywall**
|
||||
with the gimmick price struck through (`Rp{gimmick_price}`) and the actual
|
||||
price prominent (`Rp{actual_price}`). CTA: `mulai · Rp{actual_price}`.
|
||||
- Tapping CTA routes them to the existing payment flow (Cara bayar →
|
||||
Waiting → Paid) with `mode='chat'`, `duration_minutes=N`, `price_idr=actual`,
|
||||
and `is_first_session_discount=true` flagged on the payment session.
|
||||
- Ineligible users (anon path or verified-but-already-consulted) skip S6 and
|
||||
go directly to the Pemilihan Harga picker.
|
||||
|
||||
### 3.5.4 Backend
|
||||
- `payment_sessions` schema gains a column
|
||||
`is_first_session_discount BOOLEAN NOT NULL DEFAULT false` (replaces
|
||||
`is_free_trial`). Migration drops `is_free_trial` (or aliases it during
|
||||
cutover — confirm in plan).
|
||||
- Pricing endpoint exposes the discount block (see §16 below).
|
||||
|
||||
## 4. Payment-method selection + waiting + expired
|
||||
|
||||
> Figma: `screens/extras.jsx::SPaymentMethod`, `SWaitingPayment`,
|
||||
> `screens/v4.jsx::PaymentExpiredV4`.
|
||||
|
||||
### 4.1 Method screen
|
||||
- QRIS marked "DIREKOMENDASIKAN" + 4 e-wallet options
|
||||
(`gopay/ovo/dana/shopee`). Total amount + duration label rendered above.
|
||||
- CTA: `bayar Rp{amount}` → calls existing payment-creation API (mock for now).
|
||||
|
||||
### 4.2 Waiting screen
|
||||
- Renders the QRIS code, amount, and a **20-minute countdown** in
|
||||
`JetBrains Mono` styling (matches Figma `SWaitingPayment`).
|
||||
- Polls payment status every **3 seconds** while foregrounded; FCM webhook can
|
||||
short-circuit. Stop polling on background; resume on foreground.
|
||||
|
||||
### 4.3 Expired screen
|
||||
- When countdown hits 0 with no `paid` event, replace screen with
|
||||
`PaymentExpiredV4` content; CTA `coba lagi` returns to method-selection with
|
||||
the duration pre-selected.
|
||||
|
||||
### 4.4 Backend additions
|
||||
- `POST /api/client/payments/{id}/status-poll` (or reuse existing) returns one
|
||||
of `pending | paid | expired`. Mock provider flips to `paid` on a manual
|
||||
control-center button (already exists from phase 3.7) or after 20 min →
|
||||
`expired`.
|
||||
|
||||
## 5. Notification gate + home banner
|
||||
|
||||
> Figma: `screens/extras.jsx::SNotifGate` (full screen) + `screens/v3.jsx::HBNotifBanner` (home banner).
|
||||
|
||||
### 5.1 Gate screen
|
||||
- Shown **after payment succeeds and before searching** (per
|
||||
flow_customer.md §5.1.5). If OS reports notifications already allowed, skip.
|
||||
- Two CTAs: **`izinkan notifikasi`** (triggers OS prompt; on grant → next; on
|
||||
deny → fall-through to next anyway) and **`nanti aja`**.
|
||||
- Implementation note: use existing FCM bootstrap; never re-prompt within the
|
||||
same install if user denied (OS rules).
|
||||
|
||||
### 5.2 Home banner
|
||||
- If notifications are denied **and** user is on Home, show a thin amber banner
|
||||
above the fold: "aktifkan notif biar kamu enggak ketinggalan" + button
|
||||
`nyalain`.
|
||||
- Banner is dismissable for the session but reappears on next cold start until
|
||||
the user enables notifications.
|
||||
|
||||
## 6. S7 Soft-prompt + searching + 5-min timeout
|
||||
|
||||
> Figma: `screens/session.jsx::S7Prompt` (warmup) +
|
||||
> `screens/v3.jsx::SSearchPrompt` (searching/timeout combined).
|
||||
|
||||
### 6.1 Warmup prompt
|
||||
- After Notif Gate, show a single screen with three sample reflective prompts
|
||||
+ CTA `aku ngerti, lanjut →`. Tapping fires the actual blast.
|
||||
- Skip button is allowed.
|
||||
|
||||
### 6.2 Searching state
|
||||
- Reuse current `searching_screen.dart` flow but visually swap to the v3
|
||||
pulsing-dots panel + brand colors.
|
||||
- Server-side timer is unchanged (5 min).
|
||||
|
||||
### 6.3 5-min timeout
|
||||
- When 5-min timer expires with no match:
|
||||
- Replace dots with `🌙` icon and copy "bestie lagi rame".
|
||||
- Add CTAs: primary `coba cari lagi` (re-fires blast); ghost `kembali ke home` (pops to Home).
|
||||
- Tracking: log `pairing_timeout_v2` to existing analytics path.
|
||||
|
||||
## 7. Match-found visuals (S9)
|
||||
|
||||
- Existing `bestie_found_screen.dart` is upgraded to render the v4 `S9MatchV4`
|
||||
layout: orb + status dot + greeting copy `halo, aku bestie {name}` + CTA
|
||||
`mulai sesi N menit →` (where `N` is bound to the picked duration).
|
||||
- No backend change.
|
||||
|
||||
## 8. Chat-room countdown UX
|
||||
|
||||
> Figma: `screens/session.jsx::S10Chat` low-time branch +
|
||||
> `screens/v3.jsx::HBSnackbar` + `HBChatExpiredBanner`.
|
||||
|
||||
### 8.1 3-minutes-left snackbar
|
||||
- Backend already emits a `session_warning` event at 3 min remaining (Phase 3 —
|
||||
verify in `backend/src/services/session-timer.service.js`); if absent, add it.
|
||||
- Client renders a snackbar with copy "sisa 3 menit lagi ya 🤍". Auto-dismiss
|
||||
after 4 s. Only fired **once per session**.
|
||||
|
||||
### 8.2 Last-2-minutes visuals
|
||||
- When `seconds_left ≤ 120`, the timer pill in the chat header switches to
|
||||
danger color (`#FFE8D9` background, `#A8410E` text) and the progress bar at
|
||||
the bottom of the header turns orange.
|
||||
- This is a pure UI flip on a value the client already has.
|
||||
|
||||
### 8.3 Expired floating banner
|
||||
- When `seconds_left == 0` and the session is in a **closing-grace** window
|
||||
(existing concept from Phase 3 session-end overhaul, see memory
|
||||
"Phase 3 Session-End Overhaul"), inject a floating banner above the input
|
||||
bar: "habis nih... mau lanjutin curhat sama {name}?" + inline CTA `perpanjang`.
|
||||
- Tapping `perpanjang` opens the **Time-up Bottom Sheet** (next section).
|
||||
|
||||
## 9. Time-up bottom sheet (extension)
|
||||
|
||||
> Figma: `screens/extras.jsx::STimeUpSheet`.
|
||||
|
||||
### 9.1
|
||||
- Replace the current `pricing_bottom_sheet.dart` with the 5-option time-up
|
||||
sheet that includes the **chat / call toggle** (with strike-through chat
|
||||
price next to call price for transparency).
|
||||
- CTA copy: `perpanjang · Rp{price}` (primary) and `cukup, akhiri sesi`
|
||||
(secondary, opens 2-step end-of-session flow).
|
||||
|
||||
### 9.2 Targeted re-pay
|
||||
- Tapping `perpanjang` reuses the current Phase 3.7 extension API (no blast,
|
||||
same mitra) — this part is already shipped. Only the sheet UI changes.
|
||||
|
||||
## 10. End-of-session 2-step confirm + closing message + S11
|
||||
|
||||
> Figma: `screens/v3.jsx::HBConfirmEndPopup` (steps 1 + 2) +
|
||||
> `screens/extras.jsx::SClosingSheet` + `screens/session.jsx::S11Post`.
|
||||
|
||||
### 10.1 Step-1 popup
|
||||
- "beneran udah cukup?" + body. Primary `lanjut akhiri`, secondary `gak jadi, balik` → returns to Time-up sheet.
|
||||
|
||||
### 10.2 Step-2 popup
|
||||
- Only fires after step 1's `lanjut akhiri`.
|
||||
- "mau tinggalin pesan penutup?" — primary `tulis pesan penutup` opens the
|
||||
closing-message bottom sheet; secondary `lewati saja` skips to S11.
|
||||
|
||||
### 10.3 Closing message sheet
|
||||
- Textarea + two CTAs: primary `kirim & akhiri sesi` (sends a final WS message
|
||||
flagged `is_closing=true`, then closes session), secondary
|
||||
`lewat — langsung akhiri` (closes without sending).
|
||||
|
||||
### 10.4 S11 thank-you
|
||||
- New screen rendered after the WS close ack: 🌷 emoji + "terima kasih udah cerita" + CTA `balik ke home`.
|
||||
- This replaces the current "jump straight to Home" behaviour.
|
||||
|
||||
### 10.5 Mitra rejects close (rare)
|
||||
- If the mitra-side returns a 409 on the close-with-message request (e.g.
|
||||
mitra already closed it), surface the existing **Bestie Offline Popup**
|
||||
(returning variant). No new asset.
|
||||
|
||||
## 11. Returning-user pairing (bestie lama / bestie baru)
|
||||
|
||||
> Figma: `screens/v4.jsx::BestieChoiceSheet`, `BestieHistoryList`,
|
||||
> `BestieOfflinePopup` + `screens/extras.jsx::SWaitingBestie`.
|
||||
|
||||
### 11.1 Choice sheet
|
||||
- The home CTA `curhat sama bestie baru` opens the **Bestie Choice Sheet**
|
||||
(not the searching screen directly).
|
||||
- Two CTAs: `bestie yang udah kenal` → Bestie History List;
|
||||
`bestie baru` → S7 soft-prompt + blast.
|
||||
|
||||
### 11.2 History list
|
||||
- Reuse existing `chat_history_screen.dart` data + visual upgrade to the
|
||||
Figma layout: orb + bestie name + last-session date + topic + sessions count
|
||||
+ ONLINE pill (if currently online).
|
||||
|
||||
### 11.3 Targeted-wait overlay
|
||||
- Tapping a bestie in the list:
|
||||
- **Online** → calls existing targeted-pair API (Phase 3.7) and opens the
|
||||
`SWaitingBestie` overlay with the **20s mitra-approval countdown** that
|
||||
Phase 3.7 already gives us via WS but doesn't render. Three sub-states
|
||||
rendered (`waiting`, `accepted`, `declined`).
|
||||
- **Offline** → opens `BestieOfflinePopup` (returning variant). Two CTAs:
|
||||
`cari bestie lain` → blast flow; `tanya admin` → Tanya Admin sheet.
|
||||
|
||||
## 12. Tanya admin sheet
|
||||
|
||||
> Figma: `screens/v3.jsx::HBContactAdminSheet`.
|
||||
|
||||
- WhatsApp + Telegram options.
|
||||
- Numbers/handles configured in `control_center` (new readonly tiles), pulled
|
||||
by `GET /api/client/support-handles` (or hard-coded constants for v1 — pick
|
||||
hard-coded for first ship to keep it light).
|
||||
|
||||
## 13. OTP-blocked popup
|
||||
|
||||
> Figma: `screens/v4.jsx::OTPBlockedPopup`. Not in flow_customer.md but matches
|
||||
> the existing OTP rate-limit followups (memory: `OTP Rate Limit Followups`).
|
||||
|
||||
- When OTP attempts are exhausted (existing 429 from
|
||||
`backend/src/services/otp.service.js`), show the popup instead of an inline
|
||||
error.
|
||||
- Primary CTA `lanjut tanpa verif` jumps the user to the anonymous branch
|
||||
(Pilih cara → Pemilihan harga → Cara bayar) **without re-asking for ESP/USP**
|
||||
— the values entered earlier in the same OTP attempt are carried over.
|
||||
- Secondary CTA `hubungi admin` opens Tanya Admin sheet.
|
||||
|
||||
## 14. Auth Provider Auto-detection (server-driven)
|
||||
|
||||
> Replaces the existing `--dart-define=ENABLE_SOCIAL_AUTH` build flag (per
|
||||
> memory `client_app/CLAUDE.md`). Backend now decides; client_app reads.
|
||||
|
||||
### 14.1 Backend probes env at boot
|
||||
- `backend/src/services/auth-providers.service.js` (new) reads on cold start:
|
||||
- Google OAuth → enabled if both `GOOGLE_OAUTH_CLIENT_ID` and
|
||||
`GOOGLE_OAUTH_CLIENT_SECRET` are set and non-empty.
|
||||
- Apple OAuth → enabled if all required Apple keys
|
||||
(`APPLE_OAUTH_CLIENT_ID`, `APPLE_OAUTH_TEAM_ID`, `APPLE_OAUTH_KEY_ID`,
|
||||
`APPLE_OAUTH_PRIVATE_KEY`) are set and non-empty.
|
||||
- Phone OTP → always enabled (existing).
|
||||
- The result is cached in memory; no DB hit on the read endpoint.
|
||||
|
||||
### 14.2 New endpoint
|
||||
- `GET /api/shared/auth-providers` →
|
||||
```json
|
||||
{ "google": { "enabled": false },
|
||||
"apple": { "enabled": false },
|
||||
"phone": { "enabled": true } }
|
||||
```
|
||||
- Public, no auth header required.
|
||||
|
||||
### 14.3 client_app integration
|
||||
- A new `authProvidersProvider` (Riverpod) fetches once on app start +
|
||||
caches.
|
||||
- Welcome / login / register screens hide the Google/Apple buttons whose
|
||||
flag is `false`. If both social providers are off, only the phone-OTP CTA
|
||||
is shown.
|
||||
- The `--dart-define=ENABLE_SOCIAL_AUTH` flag is **removed**. Update
|
||||
`social_auth_enabled.dart` to read the provider flags instead, or delete
|
||||
it and inline the check.
|
||||
- Memory entry `client_app/CLAUDE.md` will be updated by the implementer to
|
||||
remove the build-flag reference.
|
||||
|
||||
### 14.4 mitra_app
|
||||
- mitra_app remains OTP-only (no change to its login screen). The endpoint
|
||||
exists in `/api/shared/` but mitra_app doesn't need to call it.
|
||||
|
||||
## 15. Voice-call mode indicator on chat
|
||||
|
||||
> Voice-call sessions use the **same WebSocket chat** infrastructure as
|
||||
> regular chat. The only differences are price group, header badge, and the
|
||||
> ops convention that the mitra shares a Google Meet link as a normal chat
|
||||
> message. **No call media handling. No link validation.**
|
||||
|
||||
### 15.1 Schema
|
||||
- `payment_sessions` gains a `mode TEXT NOT NULL DEFAULT 'chat'
|
||||
CHECK (mode IN ('chat', 'call'))`.
|
||||
- `chat_sessions` reads `mode` via the existing `payment_session_id` FK
|
||||
(added in Phase 3.7); no new column on `chat_sessions`.
|
||||
|
||||
### 15.2 Customer chat header
|
||||
- When `chat_sessions.mode == 'call'`, render a `📞 Voice Call` pill next to
|
||||
the bestie name in the header (replacing or appended to the existing
|
||||
status row).
|
||||
- When `'chat'`, render `💬 Chat` (or render nothing — design choice).
|
||||
- All other chat UI is unchanged.
|
||||
|
||||
### 15.3 Mitra chat header
|
||||
- Same badge logic on the mitra-side chat screen. **No composer helper for
|
||||
Meet links** — mitra types/pastes the URL as a normal message.
|
||||
|
||||
### 15.4 Link rendering
|
||||
- Both apps already render URLs in chat messages as tap-to-open. Confirm
|
||||
Google Meet links (`meet.google.com/...`) launch the OS handler / app.
|
||||
- No special "join call" badge on the message bubble — it's just a URL.
|
||||
|
||||
### 15.5 Out of scope for Phase 4
|
||||
- Generating the Meet link server-side.
|
||||
- Validating/checking the link before send.
|
||||
- Rich preview cards for Meet links.
|
||||
- Audio/video session inside the app.
|
||||
|
||||
---
|
||||
|
||||
# Backend Surface Changes (additive)
|
||||
|
||||
| Endpoint | Status | Purpose |
|
||||
|---|---|---|
|
||||
| `GET /api/client/onboarding-state` | new | drives Verif-vs-Anon sheet visibility + first-session-discount eligibility |
|
||||
| `GET /api/client/chat-pricing` | rewrite | returns chat + call tier groups + first-session-discount block (replaces `mode_multiplier` design) |
|
||||
| `GET /api/client/support-handles` | new | WA/Telegram pulled from CC config |
|
||||
| `GET /api/shared/auth-providers` | new | per-provider enabled flags; replaces `ENABLE_SOCIAL_AUTH` build flag |
|
||||
| `POST /api/client/payments/{id}/status-poll` | clarify | already mock; document polling contract |
|
||||
| `WS event session_warning` | confirm | 3-min remaining ping (verify already emitted) |
|
||||
|
||||
**Schema changes** (one migration):
|
||||
- `payment_sessions.is_free_trial BOOLEAN` → drop and replace with
|
||||
`is_first_session_discount BOOLEAN NOT NULL DEFAULT false`.
|
||||
- `payment_sessions.mode TEXT NOT NULL DEFAULT 'chat' CHECK (mode IN ('chat','call'))` (new column).
|
||||
- `chat_sessions.topics TEXT[] NULL` (new column — stores ESP picks for info display).
|
||||
|
||||
**New `app_config` rows** (seeded):
|
||||
- `first_session_discount_enabled` = `true`
|
||||
- `first_session_discount_actual_price_idr` = `2000`
|
||||
- `first_session_discount_gimmick_price_idr` = `12000`
|
||||
- `first_session_discount_duration_minutes` = `12`
|
||||
- `first_session_discount_modes` = `["chat"]`
|
||||
- `pricing_chat_tiers_json` = JSON array of chat tiers
|
||||
- `pricing_call_tiers_json` = JSON array of call tiers
|
||||
- `support_handles_json` = WA + Telegram deeplinks
|
||||
- `searching_timeout_minutes` = `5`
|
||||
- `end_session_two_step_confirm` = `true`
|
||||
- `three_minute_warning_enabled` = `true`
|
||||
|
||||
---
|
||||
|
||||
# UX Guards (carry over from Figma README)
|
||||
|
||||
These are non-negotiable per `Figma/handoff/README.md`:
|
||||
|
||||
- Two-step `akhiri sesi` confirmation. Single tap = accident.
|
||||
- Payment confirmation popup before redirect to the (mock) Xendit checkout.
|
||||
- 5-minute timeout on bestie search before suggesting alternatives.
|
||||
- OTP input never auto-advances on paste — user must verify before submit.
|
||||
- All taps ≥ 44×44.
|
||||
- Color is never the only signal (timer also bolds; snackbar also iconified).
|
||||
- Lowercase Bahasa Indonesia copy by design — apps must `text-transform: none`.
|
||||
- Out-of-scope: rating system on S11, mid-chat "akhiri sesi" button, "darurat / SOS" button, subscription tiers.
|
||||
|
||||
---
|
||||
|
||||
# Acceptance Criteria
|
||||
|
||||
A real-device run on emulator + physical phone passes all of these:
|
||||
|
||||
1. Fresh install: S1 → Home (no JWT) → CTA → S2 Nama → **Verif Choice Sheet** → both branches reach Notif Gate.
|
||||
2. Verified branch (first time): ESP (≥1 chip) → USP → S3a → S3b → **S6 Discounted Paywall** showing struck-through `Rp{gimmick}` and prominent `Rp{actual}` → Cara Bayar → Waiting (20-min clock visible) → Paid → Notif Gate.
|
||||
3. Verified branch (already consulted): same path **but skips S6** and goes straight to Pemilihan Harga.
|
||||
4. Anonymous branch: ESP → USP → Pilih cara curhat → Pemilihan Harga (chat tier list) → toggle to call → tier list rebuilds with call prices → Cara Bayar.
|
||||
5. OTP exhausted: blocked popup appears; tapping "lanjut tanpa verif" reaches Pemilihan Harga **with previously-picked ESP/USP carried over** (no re-prompt).
|
||||
6. Payment expired: 20-min timer hits 0 → expired screen → retry returns to method.
|
||||
7. Notif denied: full-screen gate's "nanti aja" closes; Home shows the amber banner; reopening Home after grant clears the banner.
|
||||
8. Soft-prompt → blast → 5-min timeout: timeout state shows the two CTAs and their behaviors are correct.
|
||||
9. Match found: S9 renders bestie name + duration in CTA copy.
|
||||
10. Chat (mode=chat): header shows `💬 Chat` (or no badge); at 3 min remaining, snackbar fires once; at 2 min remaining, timer pill turns danger; at 0, expired banner appears with `perpanjang` CTA.
|
||||
11. Chat (mode=call): header shows `📞 Voice Call` badge. Mitra pastes a `meet.google.com` URL → renders as a tappable link → tap launches the OS handler.
|
||||
12. Perpanjang: time-up sheet renders with chat/call toggle. Toggling rebuilds prices from the corresponding group.
|
||||
13. Akhiri: 2-step popup → closing message sheet → S11 thank-you → Home.
|
||||
14. Returning user: Home CTA opens Bestie Choice Sheet. `bestie yang udah kenal` → list → online → SWaitingBestie 20s overlay → match.
|
||||
15. Returning, offline: `bestie offline popup` shown; `tanya admin` opens WA/Tele sheet.
|
||||
16. Returning, mitra rejects: surfaces same offline popup with "cari bestie lain" CTA.
|
||||
17. Auth providers: with no Google/Apple env vars set, Welcome screen shows only the phone-OTP CTA. Setting `GOOGLE_OAUTH_CLIENT_ID/SECRET` and restarting backend → Google button appears on next app launch (after `authProvidersProvider` refresh).
|
||||
18. CC config knobs: an ops user toggling `first_session_discount_enabled=false` in CC → next eligible user no longer sees S6, goes to Pemilihan Harga.
|
||||
|
||||
---
|
||||
|
||||
# Test Plan
|
||||
|
||||
- **Backend:** Vitest cases for any new endpoints (per memory `Test Infrastructure`).
|
||||
- **client_app:** Maestro flows added under `client_app/.maestro/flows/`:
|
||||
- `02_onboarding_verified.yaml`
|
||||
- `03_onboarding_anon.yaml`
|
||||
- `04_payment_expired.yaml`
|
||||
- `05_searching_timeout.yaml`
|
||||
- `06_chat_countdown.yaml`
|
||||
- `07_end_session_2step.yaml`
|
||||
- `08_returning_targeted.yaml`
|
||||
- **control_center:** Playwright smoke that the existing pricing-tier admin
|
||||
view still works (no UI change but extended response shape).
|
||||
|
||||
OTP smoke (existing `01_smoke.yaml`) must keep passing — uses the dev-only
|
||||
`/internal/_test/peek-otp` + `/reset-phone` endpoints (memory:
|
||||
`OTP Test Infrastructure`).
|
||||
|
||||
---
|
||||
|
||||
# Audit Snapshot (frozen 2026-05-09)
|
||||
|
||||
> Source: a one-shot audit at the time this PRD was written. File paths are
|
||||
> relative to `client_app/`. Re-run before kickoff.
|
||||
|
||||
### Onboarding & Auth
|
||||
- 🟢 S1 Splash · `lib/features/splash/splash_screen.dart`
|
||||
- 🟡 Home (1st time) — needs notif-banner variant · `lib/features/home/home_screen.dart`
|
||||
- 🟢 Home (returning) · `lib/features/home/home_screen.dart`
|
||||
- 🟢 S2 Nama · `lib/features/auth/screens/display_name_screen.dart`
|
||||
- 🟡 ESP — currently binary, needs multi-select · `lib/features/chat/widgets/topic_selection_bottom_sheet.dart`
|
||||
- 🔴 S5b USP screen
|
||||
- 🔴 Verif-vs-Anon Choice Sheet
|
||||
- 🟡 S3a/S3b — currently 6-digit, needs 4-digit per Figma · `lib/features/auth/screens/{register,otp}_screen.dart`
|
||||
- 🔴 OTP-blocked popup → fallback to anon
|
||||
|
||||
### Payment
|
||||
- 🟡 S6 Rp2k Paywall — exists as generic pricing; copy/visuals diverge · `lib/features/payment/screens/payment_screen.dart`
|
||||
- 🟡 Pemilihan Harga — exists, missing call-mode toggle
|
||||
- 🔴 Pilih cara curhat (chat / call)
|
||||
- 🔴 Cara bayar (QRIS-first method picker)
|
||||
- 🔴 Waiting Payment (20-min QRIS)
|
||||
- 🔴 Pembayaran expired
|
||||
|
||||
### Pairing & Match
|
||||
- 🔴 Notif Gate (full screen)
|
||||
- 🔴 Home notif banner
|
||||
- 🟡 S7 Soft-prompt + Searching · `lib/features/chat/screens/searching_screen.dart`
|
||||
- 🔴 S7 Timeout 5 menit (CTAs)
|
||||
- 🟡 S9 Match · `lib/features/chat/screens/bestie_found_screen.dart`
|
||||
|
||||
### Chat Session
|
||||
- 🟡 S10 Chat · `lib/features/chat/screens/chat_screen.dart` (no countdown UX)
|
||||
- 🔴 3-min snackbar
|
||||
- 🔴 Last-2-min danger visuals
|
||||
- 🔴 Floating expired banner
|
||||
|
||||
### Session-end
|
||||
- 🟡 Confirm-akhiri popup (only step 1 today)
|
||||
- 🔴 Confirm-akhiri popup step 2
|
||||
- 🟡 Closing message — exists in goodbye composer, not as bottom sheet
|
||||
- 🔴 S11 thank-you screen
|
||||
|
||||
### Returning user
|
||||
- 🟢 Bestie history · `lib/features/chat/screens/chat_history_screen.dart`
|
||||
- 🔴 Bestie Choice Sheet
|
||||
- 🟡 Targeted-wait overlay (data exists in `pairing_notifier.dart`, no UI)
|
||||
- 🟢 Bestie Offline Popup (returning) · `lib/features/chat/widgets/bestie_unavailable_dialog.dart`
|
||||
- 🔴 Tanya Admin sheet (WA/Telegram)
|
||||
|
||||
---
|
||||
|
||||
# Implementation Order (suggested)
|
||||
|
||||
1. **Backend stubs first**: `onboarding-state`, pricing `mode_multiplier`,
|
||||
confirm `session_warning` 3-min ping, support-handles (or constants).
|
||||
2. **Onboarding shell**: Verif Choice Sheet + ESP (multi-select) + USP. Cuts
|
||||
over the existing topic-selection sheet without breaking pairing.
|
||||
3. **Payment shell**: Pilih cara → Pemilihan harga (with toggle) → Cara bayar
|
||||
→ Waiting → Expired. Wire to existing mock payment API.
|
||||
4. **Notif gate + home banner**.
|
||||
5. **Searching upgrades**: soft-prompt + 5-min timeout state.
|
||||
6. **Chat countdown UX**: 3-min snackbar, last-2-min danger, expired banner.
|
||||
7. **Session-end**: 2-step popups + closing-message sheet + S11.
|
||||
8. **Returning user**: Choice Sheet + targeted-wait overlay + Tanya Admin.
|
||||
9. **Edge cases**: OTP-blocked popup; mitra rejects close.
|
||||
10. **Maestro coverage** + visual regression sweep.
|
||||
|
||||
Each block is shippable independently — they share no breaking schema change.
|
||||
Reference in New Issue
Block a user