Files
halobestie-clone/client_app/lib/features/payment/screens/payment_entry_screen.dart
Ramadhan Sjamsani 093256ff7d 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>
2026-05-18 21:50:04 +08:00

79 lines
2.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/auth/onboarding_intent_provider.dart';
import '../../../core/chat/chat_opening_provider.dart';
import '../../../core/theme/halo_tokens.dart';
import '../state/payment_draft_provider.dart';
/// Single point of truth for the discount-vs-method-pick branch.
///
/// Reads `chat-pricing.first_session_discount.eligible`. When the customer
/// is eligible (and the discount is enabled), routes to the S6 paywall;
/// otherwise routes to the regular method-pick screen. The draft is reset
/// here so a fresh entry into the flow always starts clean.
class PaymentEntryScreen extends ConsumerStatefulWidget {
const PaymentEntryScreen({super.key});
@override
ConsumerState<PaymentEntryScreen> createState() => _PaymentEntryScreenState();
}
class _PaymentEntryScreenState extends ConsumerState<PaymentEntryScreen> {
bool _routed = false;
@override
void initState() {
super.initState();
Future.microtask(() {
if (!mounted) return;
// Targeting is set BEFORE this screen (by bestie-history-list) and must
// survive the entry-screen reset, so use resetExceptTarget() — full
// reset() would wipe targetedMitraId and silently downgrade the
// returning-targeted flow to a blast.
ref.read(paymentDraftNotifierProvider.notifier).resetExceptTarget();
// Consume the onboarding intent — landing here means the router-level
// post-OTP redirect has fired (or the user navigated in via another
// CTA). Reset to default so a later masuk → recovery flow doesn't
// inherit a stale onboarding intent.
ref.read(onboardingIntentProvider.notifier).state =
OnboardingIntent.recover;
});
}
void _routeOnce(String location) {
if (_routed || !mounted) return;
_routed = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.go(location);
});
}
@override
Widget build(BuildContext context) {
final pricingAsync = ref.watch(chatPricingProvider);
return Scaffold(
backgroundColor: HaloTokens.bg,
body: pricingAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) {
// Pricing fetch failed — fall through to method-pick (which fetches
// pricing again and surfaces the error there).
_routeOnce('/payment/method-pick');
return const SizedBox.shrink();
},
data: (pricing) {
if (pricing.firstSessionDiscount?.eligible ?? false) {
_routeOnce('/payment/discount-paywall');
} else {
_routeOnce('/payment/method-pick');
}
return const SizedBox.shrink();
},
),
);
}
}