Phase 4 Stage 2: onboarding redesign (client_app + mitra_app)
Verif Choice Sheet on display_name_screen drives the user into either the verified or anonymous onboarding sub-flow. ESP screen (12 chips, multi-select, info-only) + USP screen are shared between both branches; selections persist through to chat_sessions.topics on session start. OTP-blocked popup (HaloPopup) listens for the four real OTP-rate-limit error codes (OTP_RATE_LIMIT_PHONE, OTP_RATE_LIMIT_IP, OTP_COOLDOWN, OTP_ATTEMPTS_EXCEEDED) and drops the user onto the anonymous path with ESP/USP state preserved. Auth-providers gating replaces the --dart-define=ENABLE_SOCIAL_AUTH build flag with server-driven discovery. authProvidersProvider preloads GET /api/shared/auth-providers at cold start; welcome/register/ force-register screens render Google/Apple buttons only when the backend reports enabled:true. Falls back to phone-OTP-only when both providers are off. social_auth_enabled.dart deleted; client_app/CLAUDE.md updated to reflect the new gating contract. Mitra app: chat screen renders an ESP chip strip above the first message bubble when chat_sessions.topics is non-empty. Backend session.service.js getSessionById SELECTs cs.topics so the mitra side can read the customer's selected topics. Maestro flows 02_onboarding_verified.yaml + 03_onboarding_anon.yaml. Deviation from plan: plan referenced OTP error code 'otp_retry_exhausted'; real codes are OTP_RATE_LIMIT_*/OTP_COOLDOWN/OTP_ATTEMPTS_EXCEEDED - popup listens for all four. Plan said 'has_paid_first_session'; live endpoint returns 'has_consulted_before' - used the live field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import 'features/auth/screens/otp_screen.dart';
|
||||
import 'features/auth/screens/force_register_screen.dart';
|
||||
import 'features/auth/screens/set_display_name_screen.dart';
|
||||
import 'features/onboarding/onboarding_screen.dart';
|
||||
import 'features/onboarding/screens/esp_screen.dart';
|
||||
import 'features/onboarding/screens/usp_screen.dart';
|
||||
import 'features/splash/splash_screen.dart';
|
||||
import 'features/home/home_screen.dart';
|
||||
import 'core/constants.dart';
|
||||
@@ -59,6 +61,11 @@ GoRouter buildRouter(Ref ref) {
|
||||
final isOnboarding = state.matchedLocation == '/onboarding';
|
||||
final isAuthRoute = state.matchedLocation.startsWith('/auth') ||
|
||||
state.matchedLocation == '/welcome';
|
||||
// Phase 4 onboarding flow (Verif Choice → ESP → USP) — must transit
|
||||
// freely while authState is AuthAnonymousData so the router doesn't
|
||||
// boot the user back to /home before they finish onboarding.
|
||||
final isOnboardingFlow =
|
||||
state.matchedLocation.startsWith('/onboarding/');
|
||||
|
||||
// Show splash only during initial load
|
||||
if (authState is AsyncLoading) {
|
||||
@@ -84,6 +91,18 @@ GoRouter buildRouter(Ref ref) {
|
||||
}
|
||||
|
||||
if (data is AuthAuthenticatedData || data is AuthAnonymousData) {
|
||||
// Allow the Phase 4 onboarding flow (ESP/USP) to stay put even when
|
||||
// the user is already anonymous-authenticated — display_name_screen
|
||||
// intentionally pushes into /onboarding/* after loginAnonymous.
|
||||
if (isOnboardingFlow) return null;
|
||||
// display_name_screen owns the post-anonymous-login routing decision
|
||||
// (onboarding-state lookup → Verif Choice Sheet vs returning-user
|
||||
// jump). Don't preempt it by redirecting to /home the instant the
|
||||
// anonymous login resolves — wait until the screen pushes onward.
|
||||
if (data is AuthAnonymousData &&
|
||||
state.matchedLocation == '/auth/display-name') {
|
||||
return null;
|
||||
}
|
||||
return (isSplash || isAuthRoute) ? '/home' : null;
|
||||
}
|
||||
if (data is AuthNeedsDisplayNameData) return '/auth/set-name';
|
||||
@@ -103,6 +122,32 @@ GoRouter buildRouter(Ref ref) {
|
||||
GoRoute(path: '/auth/otp', builder: (context, state) => OtpScreen(phone: state.extra as String)),
|
||||
GoRoute(path: '/auth/set-name', builder: (_, __) => const SetDisplayNameScreen()),
|
||||
GoRoute(path: '/auth/force-register', builder: (_, __) => const ForceRegisterScreen()),
|
||||
// Phase 4 onboarding sub-flow (Stage 2). Verified vs anonymous branch
|
||||
// share ESP + USP screens; the parent path drives the post-USP fork.
|
||||
GoRoute(
|
||||
path: '/onboarding/verif/esp',
|
||||
builder: (_, __) => const EspScreen(verified: true),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/onboarding/verif/usp',
|
||||
builder: (_, __) => const UspScreen(verified: true),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/onboarding/anon/esp',
|
||||
builder: (_, __) => const EspScreen(verified: false),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/onboarding/anon/usp',
|
||||
builder: (_, __) => const UspScreen(verified: false),
|
||||
),
|
||||
// Alias for the OTP-blocked popup's "lanjut tanpa verif" exit. The
|
||||
// popup may fire from any point in the verified branch (after the
|
||||
// user has already passed ESP+USP), so we expose a stable terminal
|
||||
// landing-zone alias rather than rewriting all upstream pushes.
|
||||
GoRoute(
|
||||
path: '/onboarding/anon/method',
|
||||
redirect: (_, __) => '/payment/method-pick',
|
||||
),
|
||||
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
|
||||
GoRoute(path: '/payment', builder: (context, state) {
|
||||
// Payment screen reachable from
|
||||
|
||||
Reference in New Issue
Block a user