Phase 4 §2 + §1/§4: OnboardingIntent post-OTP routing + test naming + register-screen overflow

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>
This commit is contained in:
2026-05-18 21:50:04 +08:00
parent 938954bbe8
commit 093256ff7d
22 changed files with 666 additions and 76 deletions

View File

@@ -241,6 +241,12 @@ class Auth extends _$Auth {
}
Future<void> verifyOtp(String otpRequestId, String code) async {
// Preserve the prior auth data (typically AuthAnonymousData from the
// pre-OTP loginAnonymous) so AsyncError keeps `valueOrNull` non-null.
// The router uses valueOrNull to gate redirects — a wipe-to-null on
// OTP failure would bounce the OTP-blocked recovery path
// (/onboarding/anon/method → /payment/method-pick) to /home.
final previous = state;
state = const AsyncLoading();
try {
// Bearer is attached automatically by ApiClient from AuthBridge — when
@@ -259,12 +265,13 @@ class Auth extends _$Auth {
final profile = await _applyTokens(response);
state = AsyncData(await _stateForProfile(profile));
} on DioException catch (e) {
state = AsyncError(_otpVerifyErrorInfo(e), StackTrace.current);
state = AsyncError<AuthData>(_otpVerifyErrorInfo(e), StackTrace.current)
.copyWithPrevious(previous);
} catch (_) {
state = AsyncError(
state = AsyncError<AuthData>(
const AuthErrorInfo('Gagal verifikasi. Coba lagi.'),
StackTrace.current,
);
).copyWithPrevious(previous);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Tracks where the user came from when they entered an auth flow.
///
/// `onboarding` is set when the user taps a transaction CTA ("aku mau
/// curhat" / "curhat sama bestie baru") that drives them into the §2
/// New-User Onboarding journey. Post-OTP, the router consumes this and
/// pushes /payment/entry (which dispatches S6 paywall vs PickMethod via
/// `first_session_discount.eligible`).
///
/// `recover` (default) is the SHome1st "masuk →" login-recover banner
/// path — the spec doesn't route this through /payment/entry; the user
/// expects to land on /home with their chat history.
///
/// Spec ref: requirement/flow_customer.mermaid.md §2 (`UserLookup → S6
/// or PickMethod`).
enum OnboardingIntent { recover, onboarding }
final onboardingIntentProvider =
StateProvider<OnboardingIntent>((ref) => OnboardingIntent.recover);