Spec §2 (flow_customer.mermaid) routes post-OTP based on user-lookup + has_transacted, but the implementation previously dumped every OTP success on /home. Introduce `OnboardingIntent` provider: set to `onboarding` by routeForVerifChoice's verified branch (the "aku mau curhat" transaction journey), set to `recover` by SHome1st's masuk → banner. Router redirect on AuthAuthenticatedData+isAuthRoute consumes it: `onboarding` → /payment/entry (dispatches S6 paywall vs PickMethod via first_session_discount.eligible); `recover` → /home. Intent is reset in /payment/entry's initState so subsequent masuk → flows don't inherit it. auth_notifier.verifyOtp uses .copyWithPrevious on AsyncError so valueOrNull retains AuthOtpSentData/AuthAnonymousData through OTP failures — required for the OTP-blocked recovery path (/onboarding/anon/method → /payment/method-pick) to clear the global redirect without bouncing to /home. Router also extends the isAuthRoute/isOnboardingFlow carve-out to AuthOtpSentData. Maestro tests adopt `ts-<app>-<NN>-<MM>-<descriptor>.yaml` convention: NN = mermaid section, MM = sub-flow index. New ts-customer-02-01..05 cover the §2 branches (verified brand-new → S6, existing-no-tx → S6, existing-tx → method-pick, OTP-blocked → method-pick, anonymous first- timer → method-pick); deferred 02-06/07/08/09 documented in README_section_02.md. TS-07 → ts-customer-02-10 (masuk → recovery); TS-01..06 → ts-customer-04-01..06 (§4 returning-user). Shared onboarding_new_user_verified.yaml subflow extracted. Register screen's body Column now uses LayoutBuilder + SingleChildScrollView + ConstrainedBox + IntrinsicHeight so the keyboard-open layout no longer overflows by 1.3 px (verified visually). Spec prose updated at flow_customer.mermaid §2 to describe the intent-driven routing + login-vs-transaction divergence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
5.2 KiB
Markdown
90 lines
5.2 KiB
Markdown
# §2 — New-User Onboarding test plan
|
|
|
|
Spec: [requirement/flow_customer.mermaid.md §2 + §2.1](../../../requirement/flow_customer.mermaid.md).
|
|
|
|
Tests use the naming convention `ts-customer-<section>-<sub>-<description>.yaml`:
|
|
- `<section>` — flow_customer.mermaid section number (`02` for §2 + §2.1).
|
|
- `<sub>` — sub-flow index within the section.
|
|
- `<description>` — snake_case summary of the branch under test.
|
|
|
|
## Implemented
|
|
|
|
| File | Branch (spec ref) | Expected destination |
|
|
|---|---|---|
|
|
| `ts-customer-02-01-verified_brand_new_to_s6_paywall.yaml` | §2 verified · UserLookup=no (brand-new) | `/payment/discount-paywall` (S6) |
|
|
| `ts-customer-02-02-verified_existing_no_tx_to_s6_paywall.yaml` | §2 verified · existing customer · has_transacted=false | `/payment/discount-paywall` (S6) |
|
|
| `ts-customer-02-03-verified_existing_transacted_to_method_pick.yaml` | §2 verified · existing customer · has_transacted=true | `/payment/method-pick` (PickMethod) |
|
|
| `ts-customer-02-04-otp_blocked_fallback_anonymous_to_method_pick.yaml` | §2 verified · OTPok="too many retries" → OTPBlock → fallback | `/payment/method-pick` (PickMethod) |
|
|
| `ts-customer-02-05-anonymous_first_timer_to_method_pick.yaml` | §2 anonymous · USPGateB=first-timer → USPb | `/payment/method-pick` (PickMethod) |
|
|
| `ts-customer-02-10-recover_via_masuk_existing_user_to_home.yaml` | SHome1st "masuk →" recover · existing identified user | `/home` (returning view) |
|
|
|
|
The shared verified-onboarding prelude lives at
|
|
[`../subflows/onboarding_new_user_verified.yaml`](../subflows/onboarding_new_user_verified.yaml).
|
|
|
|
## Deferred (not yet implemented — see reasons)
|
|
|
|
### `ts-customer-02-06` — anonymous · USP already seen → PickMethod directly
|
|
**Branch:** §2 anonymous · USPGateB=seen → skip USP → PickMethod.
|
|
|
|
**Why deferred:** verifying the seen=true skip path literally requires the
|
|
user to reach `VerifChoiceSheet` while local `flutter.usp_seen=true` AND
|
|
auth state is fresh (`AuthInitialData`, no anon customer). Maestro's
|
|
`launchApp clearState:true` wipes both `SharedPreferences` and
|
|
`FlutterSecureStorage` together — there's no built-in way to clear ONLY
|
|
secure storage between passes. A two-pass test that runs USP once then
|
|
relaunches preserves both prefs and the anonymous session, so Pass 2 lands
|
|
on `SHomeReturning` instead of `SHome1st`, bypassing VerifChoiceSheet.
|
|
|
|
**Possible future approach:** add a shell-level pre-test step that runs
|
|
`adb shell run-as <pkg> rm shared_prefs/FlutterSecureStorage*.xml` and
|
|
preserves `FlutterSharedPreferences.xml`, then invoke maestro. Requires a
|
|
wrapper script or a maestro extension that can call adb.
|
|
|
|
### `ts-customer-02-07` — §2.1 5.1 anon transact → OTP new phone → upgrade in place
|
|
### `ts-customer-02-08` — §2.1 5.2 anon transact → OTP existing phone → merge (existing has_transacted=true)
|
|
### `ts-customer-02-09` — §2.1 5.2 merge · existing has_transacted=false
|
|
|
|
**Branch:** §2.1 anonymous → existing-user merge sub-flow.
|
|
|
|
**Why deferred:** §2.1's prereq is "anonymous customer has completed at
|
|
least one transaction". The app derives `has_transacted` from
|
|
`chat_sessions` rows, and the auth flow ties customer identity to the
|
|
device's `FlutterSecureStorage` refresh token. Reproducing the prereq in
|
|
Maestro requires either:
|
|
|
|
1. Driving the app UI all the way through anonymous transaction → payment
|
|
confirm → pairing → chat → end-session (a full §3 + §5 + §6 traversal),
|
|
then triggering OTP from the SHomeReturning state. The SHomeReturning
|
|
"curhat sama bestie baru" CTA bypasses `VerifChoiceSheet` entirely, so
|
|
the only OTP entry point from this state is the SHome1st "masuk →"
|
|
banner — which is only shown on SHome1st (no identity), not on
|
|
SHomeReturning.
|
|
|
|
2. Pre-seeding a customer + chat_session in the DB *and* injecting the
|
|
corresponding refresh token into the device's FlutterSecureStorage so
|
|
the app boots as that anonymous-with-transactions customer. Maestro
|
|
doesn't expose adb shell exec, so this requires a wrapper script.
|
|
|
|
In practice, the §2.1 merge logic is more naturally covered by **backend
|
|
integration tests** against `resolveCustomerForIdentity` in
|
|
`backend/src/services/auth.service.js` — the app-side behavior after
|
|
re-login is the same `/home` landing (login intent) tested by 02-10. The
|
|
merge state is a backend invariant, not a UI invariant.
|
|
|
|
## Behavior notes
|
|
|
|
- §2 verified branches route post-OTP via `/payment/entry`, which then
|
|
dispatches based on the backend's `first_session_discount.eligible`
|
|
(see `backend/src/services/pricing.service.js::isCustomerEligibleForFirstSessionDiscount`).
|
|
This handles both "brand-new" and "existing-but-never-paid" with a
|
|
single check.
|
|
- The `OnboardingIntent` provider (added 2026-05-18 with the §2 fix)
|
|
distinguishes transaction-CTA entries (`onboarding`) from login-recover
|
|
entries (`recover`). Set at `verif_choice_sheet.dart::routeForVerifChoice`
|
|
(verified branch) and at `home_screen.dart::_LoginRecoverBanner`
|
|
(masuk →). Consumed by `router.dart`'s post-OTP redirect.
|
|
- `auth_notifier.dart::verifyOtp` uses `.copyWithPrevious(previous)` when
|
|
setting `AsyncError`, so `valueOrNull` retains the prior
|
|
`AuthOtpSentData` / `AuthAnonymousData` through the OTP-blocked popup
|
|
→ fallback path.
|