# §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-
--.yaml`: - `
` — flow_customer.mermaid section number (`02` for §2 + §2.1). - `` — sub-flow index within the section. - `` — 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 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.