Phase 4 checkpoint: chat-screen perf refactor + retryable blast-failure + repo-wide dispose-ref guardrail
Chat-screen performance (customer + mitra): - Parent screens have zero `ref.watch` — only `ref.listen` for side effects - Body extracted into its own `ConsumerStatefulWidget`; AppBar parts split into narrow `.select` consumers (mode, sensitivity, timer) - Per-second timer ticks routed to dedicated providers (`chatRemainingSecondsProvider` + new `mitraChatRemainingSecondsProvider`) so WS `session_tick` frames don't invalidate the rest of the chat state Dispose-in-ref bug fix: - `home_screen.dart`, `payment_screen.dart`, `mitra_chat_screen.dart` — ref-using cleanup moved from `dispose()` to `deactivate()`. Modern Riverpod invalidates `ref` the moment `dispose()` runs; the resulting silent error corrupts the widget-tree finalize and the next screen appears frozen - `halo_lints` package added at repo root with `no_ref_in_dispose` rule to catch this pattern in CI / IDE analysis - `custom_lint` activated in both apps' `analysis_options.yaml` (was installed but never wired in — also brings `riverpod_lint`'s `avoid_ref_inside_state_dispose` online) - CLAUDE.md Pitfalls section added to client_app + mitra_app Phase 4 §3 retryable blast-failure (Option A): - Backend `expirePairingRequest` + all-rejected use `recordIntermediateFailure` instead of `failPaymentSession` so the payment session stays `confirmed` for re-blast - WS `pairing_failed` payload carries `is_terminal: false` on the retryable paths; client parses the flag and exposes `retryBlast()` - "Coba cari lagi" CTA on S7 Timeout now re-blasts on the same payment - Pairing service test updated to reflect the new semantics Customer waiting-payment screen navigation patch: - `_navigateTerminal` uses `Future.microtask` + `addPostFrameCallback` redundancy after a release-mode bug where polling stopped but `context.go` never fired, leaving the screen visually stuck on "menunggu pembayaran" See requirement/resume-2026-05-15.md for next-day pickup checklist (mitra release rebuild + S21 Ultra install + retest is the gating item). Bundles unrelated in-flight Phase 4 §2.x work that was already on disk (ESP screen removal, USP one-time gate scaffolding, bestie-availability public route, OTP service edits, Maestro flow tweaks) — kept together to avoid a partial-rebase mess. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ class _DisplayNameScreenState extends ConsumerState<DisplayNameScreen> {
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
routeForVerifChoice(context, choice);
|
||||
await routeForVerifChoice(context, ref, choice);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/auth/auth_notifier.dart';
|
||||
import '../../../core/auth/auth_providers_provider.dart';
|
||||
import '../../../core/constants.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
|
||||
/// S3a — WhatsApp input screen.
|
||||
///
|
||||
/// Visual contract is `Figma/screens/onboarding.jsx::S3Phone`:
|
||||
/// - HaloStepDots at the top (step 3 of 4: S2 Nama → S5 ESP → S5b USP → S3a)
|
||||
/// - Personalised display-title `"nomor wa-mu, {name}?"`
|
||||
/// - +62 prefix as static chip; user types only the trailing digits
|
||||
/// - Privacy reassurance card
|
||||
/// - Primary CTA `"kirim kode"` (disabled until ≥9 digits)
|
||||
/// - Ghost link `"lanjut tanpa verifikasi (harga normal)"` → anonymous path
|
||||
///
|
||||
/// Two callers route here:
|
||||
/// 1. New-user verified onboarding (USP → here) — auth state has the
|
||||
/// anonymous display_name set by S2.
|
||||
/// 2. SHome1st "masuk →" banner for returning-user recovery — auth state
|
||||
/// is initial; the name greeting falls back to "kamu".
|
||||
class RegisterScreen extends ConsumerStatefulWidget {
|
||||
const RegisterScreen({super.key});
|
||||
|
||||
@@ -19,8 +34,9 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
final _phoneController = TextEditingController();
|
||||
ProviderSubscription<AsyncValue<AuthData>>? _authSub;
|
||||
|
||||
// Server-imposed lockout: when /otp/request returns 429, the backend
|
||||
// includes retry_after_seconds. We disable "Kirim OTP" for that window.
|
||||
// Server-imposed lockout from /otp/request 429s. Backend embeds
|
||||
// retry_after_seconds in the AuthErrorInfo so we can disable the CTA
|
||||
// until the next slot opens.
|
||||
int _lockoutSeconds = 0;
|
||||
Timer? _lockoutTimer;
|
||||
String? _errorMessage;
|
||||
@@ -28,13 +44,14 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_phoneController.addListener(() => setState(() {}));
|
||||
_authSub = ref.listenManual<AsyncValue<AuthData>>(authProvider, (prev, next) {
|
||||
if (!mounted) return;
|
||||
final data = next.valueOrNull;
|
||||
if (data is AuthOtpSentData) {
|
||||
// Use go (replace) so re-submitting the phone form doesn't stack
|
||||
// multiple OtpScreen instances with active listeners.
|
||||
context.go('/auth/otp', extra: _phoneController.text.trim());
|
||||
// go (replace) so re-submitting the form doesn't stack OtpScreens
|
||||
// with leftover listeners.
|
||||
context.go('/auth/otp', extra: _e164Phone());
|
||||
return;
|
||||
}
|
||||
if (next is AsyncError) {
|
||||
@@ -76,107 +93,130 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Subscriber digits with leading zeros stripped. Users commonly type
|
||||
/// `0812…` (local format); the backend wants `+62812…`, so the 0 must go.
|
||||
String _subscriberDigits() {
|
||||
final digits = _phoneController.text.replaceAll(RegExp(r'\D'), '');
|
||||
return digits.replaceFirst(RegExp(r'^0+'), '');
|
||||
}
|
||||
|
||||
/// Local digits (no country code) → E.164 string the backend expects.
|
||||
String _e164Phone() => '+62${_subscriberDigits()}';
|
||||
|
||||
String _greetingName(AuthData? data) => switch (data) {
|
||||
AuthAnonymousData d => d.displayName,
|
||||
AuthAuthenticatedData d => (d.profile['display_name'] as String?) ?? '',
|
||||
AuthNeedsDisplayNameData d => (d.profile['display_name'] as String?) ?? '',
|
||||
_ => '',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = ref.watch(authProvider);
|
||||
final isLoading = authState is AsyncLoading;
|
||||
final isLockedOut = _lockoutSeconds > 0;
|
||||
final canSubmit = !isLoading && !isLockedOut;
|
||||
final providersAsync = ref.watch(authProvidersProvider);
|
||||
final providers =
|
||||
providersAsync.valueOrNull ?? AuthProvidersConfig.fallback;
|
||||
final hasMinDigits = _subscriberDigits().length >= 9;
|
||||
final canSubmit = hasMinDigits && !isLoading && !isLockedOut;
|
||||
|
||||
final name = _greetingName(authState.valueOrNull);
|
||||
final shownName = name.isEmpty ? 'kamu' : name;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Masuk / Daftar')),
|
||||
backgroundColor: HaloTokens.bg,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(HaloSpacing.s24),
|
||||
padding: const EdgeInsets.fromLTRB(28, 8, 28, 28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (providers.hasAnySocial) ...[
|
||||
if (providers.google) ...[
|
||||
HaloButton(
|
||||
label: 'lanjut dengan Google',
|
||||
icon: const Icon(Icons.g_mobiledata),
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () => ref.read(authProvider.notifier).loginGoogle(),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
],
|
||||
if (providers.apple) ...[
|
||||
HaloButton(
|
||||
label: 'lanjut dengan Apple',
|
||||
icon: const Icon(Icons.apple),
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () => ref.read(authProvider.notifier).loginApple(),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
],
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: HaloSpacing.s16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: HaloTokens.border)),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: HaloSpacing.s12),
|
||||
child: Text(
|
||||
'atau',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
color: HaloTokens.inkMuted,
|
||||
fontSize: 13,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 8),
|
||||
child: HaloStepDots(total: 4, current: 3),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'nomor wa-mu, $shownName?',
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.brandDark,
|
||||
height: 1.15,
|
||||
letterSpacing: -0.56,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'supaya bisa lanjut kapan aja, dan dapat harga khusus pengguna baru.',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14.5,
|
||||
color: HaloTokens.inkSoft,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_PhoneRow(
|
||||
controller: _phoneController,
|
||||
borderColor: hasMinDigits
|
||||
? HaloTokens.brand
|
||||
: HaloTokens.border,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _PrivacyCard(),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
color: HaloTokens.danger,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: HaloTokens.border)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
TextField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nomor HP',
|
||||
hintText: '+628xxxxxxxxxx',
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s16),
|
||||
HaloButton(
|
||||
label: isLoading
|
||||
? 'memproses...'
|
||||
: isLockedOut
|
||||
? 'coba lagi dalam ${formatCountdown(_lockoutSeconds)}'
|
||||
: 'kirim OTP',
|
||||
: 'kirim kode',
|
||||
fullWidth: true,
|
||||
onPressed: canSubmit
|
||||
? () {
|
||||
final phone = _phoneController.text.trim();
|
||||
if (phone.isEmpty) return;
|
||||
ref.read(authProvider.notifier).requestOtp(phone);
|
||||
}
|
||||
? () => ref
|
||||
.read(authProvider.notifier)
|
||||
.requestOtp(_e164Phone())
|
||||
: null,
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
// Skip ESPb/USPb — the verified branch already ran ESPa+USPa,
|
||||
// so the redirect alias drops the user straight at PickMethod.
|
||||
: () => context.go('/onboarding/anon/method'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: HaloTokens.inkSoft,
|
||||
minimumSize: const Size(0, 40),
|
||||
),
|
||||
child: const Text(
|
||||
'lanjut tanpa verifikasi (harga normal)',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
color: HaloTokens.danger,
|
||||
fontSize: 13,
|
||||
fontSize: 12.5,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: HaloTokens.inkSoft,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -184,3 +224,105 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhoneRow extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final Color borderColor;
|
||||
const _PhoneRow({required this.controller, required this.borderColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: HaloTokens.surface,
|
||||
borderRadius: HaloRadius.lg,
|
||||
border: Border.all(color: borderColor, width: 1.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'🇮🇩 +62',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
maxLength: 13, // enough headroom for 12-digit ID mobiles
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '812 3456 7890',
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: HaloTokens.inkMuted,
|
||||
),
|
||||
// Override the app-wide inputDecorationTheme so the input
|
||||
// sits flush inside the outer pill — no fill, no border.
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrivacyCard extends StatelessWidget {
|
||||
const _PrivacyCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: HaloTokens.brandSofter,
|
||||
borderRadius: HaloRadius.md,
|
||||
border: Border.all(color: HaloTokens.brandSoft),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('🛡️', style: TextStyle(fontSize: 14)),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'anonim — bestie cuma tau nama panggilan kamu. nomor gak akan dishare.',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 12,
|
||||
color: HaloTokens.brandDark,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/auth/auth_notifier.dart';
|
||||
import '../../../core/auth/auth_providers_provider.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
|
||||
class WelcomeScreen extends ConsumerWidget {
|
||||
const WelcomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final providersAsync = ref.watch(authProvidersProvider);
|
||||
final providers =
|
||||
providersAsync.valueOrNull ?? AuthProvidersConfig.fallback;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(HaloSpacing.s24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Center(child: HaloOrb(seed: 0, size: 96, label: 'H')),
|
||||
const SizedBox(height: HaloSpacing.s24),
|
||||
const Text(
|
||||
'Halo Bestie',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
const Text(
|
||||
'Tempat curhat kamu',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s48),
|
||||
HaloButton(
|
||||
label: 'Lanjut sebagai Tamu',
|
||||
fullWidth: true,
|
||||
onPressed: () => context.push('/auth/display-name'),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
if (providers.google) ...[
|
||||
HaloButton(
|
||||
label: 'lanjut dengan Google',
|
||||
icon: const Icon(Icons.g_mobiledata),
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
onPressed: () =>
|
||||
ref.read(authProvider.notifier).loginGoogle(),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
],
|
||||
if (providers.apple) ...[
|
||||
HaloButton(
|
||||
label: 'lanjut dengan Apple',
|
||||
icon: const Icon(Icons.apple),
|
||||
variant: HaloButtonVariant.secondary,
|
||||
fullWidth: true,
|
||||
onPressed: () =>
|
||||
ref.read(authProvider.notifier).loginApple(),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
],
|
||||
HaloButton(
|
||||
label: 'Daftar / Masuk',
|
||||
variant: HaloButtonVariant.ghost,
|
||||
fullWidth: true,
|
||||
onPressed: () => context.push('/auth/register'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,12 @@ import '../../support/widgets/tanya_admin_sheet.dart';
|
||||
/// Modal shown when OTP delivery / verification is exhausted (rate-limited
|
||||
/// 429 from `OTP_RATE_LIMIT_PHONE`, `OTP_RATE_LIMIT_IP`, `OTP_COOLDOWN`, or
|
||||
/// `OTP_ATTEMPTS_EXCEEDED`). Offers a "lanjut tanpa verif" exit into the
|
||||
/// anonymous flow (preserving any ESP/USP state) and a "hubungi admin" CTA
|
||||
/// that opens the Tanya Admin sheet.
|
||||
/// anonymous flow and a "hubungi admin" CTA that opens the Tanya Admin sheet.
|
||||
///
|
||||
/// By the time this popup can fire, the USP one-time gate has already been
|
||||
/// evaluated upstream on `VerifChoiceSheet` (either shown + marked seen, or
|
||||
/// skipped because already seen). The exit can therefore jump straight into
|
||||
/// `/payment/method-pick` regardless.
|
||||
class OtpBlockedPopup {
|
||||
const OtpBlockedPopup._();
|
||||
|
||||
@@ -35,12 +39,7 @@ class OtpBlockedPopup {
|
||||
),
|
||||
primary: HaloPopupAction(
|
||||
label: 'lanjut tanpa verif',
|
||||
onPressed: () {
|
||||
// ESP/USP picks live in Riverpod providers (espSelectionProvider,
|
||||
// espSkippedProvider) and survive this navigation — no need to pass
|
||||
// them as `extra`.
|
||||
context.go('/onboarding/anon/method');
|
||||
},
|
||||
onPressed: () => context.go('/onboarding/anon/method'),
|
||||
),
|
||||
secondary: HaloPopupAction(
|
||||
label: 'hubungi admin',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
import '../../../core/theme/widgets/widgets.dart';
|
||||
import '../../onboarding/usp_seen_provider.dart';
|
||||
|
||||
/// Result of the post-name Verif Choice Sheet. Caller routes to the matching
|
||||
/// onboarding sub-flow.
|
||||
@@ -65,13 +67,26 @@ class VerifChoiceSheet extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Helper: route to the right onboarding sub-flow for a verif choice.
|
||||
void routeForVerifChoice(BuildContext context, VerifChoice choice) {
|
||||
///
|
||||
/// Phase 4 (2026-05-12): the S5 ESP screen is retired and S5b USP is now a
|
||||
/// one-time gate. If the user has already seen USP (local SharedPreferences
|
||||
/// flag, OR-merged with `customers.usp_seen` on login), we skip USP entirely
|
||||
/// and jump to the per-branch next step.
|
||||
Future<void> routeForVerifChoice(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
VerifChoice choice,
|
||||
) async {
|
||||
final seen = await ref.read(uspSeenProvider.future);
|
||||
if (!context.mounted) return;
|
||||
switch (choice) {
|
||||
case VerifChoice.verified:
|
||||
context.push('/onboarding/verif/esp');
|
||||
context.push(seen ? '/auth/register' : '/onboarding/verif/usp');
|
||||
break;
|
||||
case VerifChoice.anonymous:
|
||||
context.push('/onboarding/anon/esp');
|
||||
// `/onboarding/anon/method` redirects to `/payment/method-pick`; use the
|
||||
// canonical destination here.
|
||||
context.push(seen ? '/payment/method-pick' : '/onboarding/anon/usp');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user