Add Firebase Analytics (GA4) funnel tracking to client_app: - AnalyticsService typed wrapper (enum-gated, no PII) + analyticsProvider - FirebaseAnalyticsObserver on GoRouter (screen_name via nameExtractor) - user_id = customer UUID, user_type property, set on auth resolve/upgrade - funnel events: curhat_start, curhat_repeat_start, auth_*, onboarding_usp_view, payment_view, payment_method_select, payment_started, pairing_matched/no_bestie - bottom-sheet events: verif_choice_view/select, bestie_choice_view/select, extension_offer_view, chat_extension_requested - payment_started carries app_instance_id + ga_session_id in the /payment-requests body for future server-side stitching (backend ignores) - curhat_mode_pick screen name disambiguates the chat/call mode picker (/payment/method-pick) from the payment-channel picker (/payment/method) - unify both home CTAs to "Aku Mau Curhat" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
3.4 KiB
Dart
92 lines
3.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../../core/analytics/analytics_service.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;
|
|
|
|
// Funnel step 8 — payment entry. A targeted mitra (set just before this
|
|
// screen by the bestie-history list) marks the repeat funnel; otherwise
|
|
// it's activation. resetExceptTarget() above preserves that flag.
|
|
final isRepeat =
|
|
ref.read(paymentDraftNotifierProvider).targetedMitraId != null;
|
|
// ignore: discarded_futures
|
|
ref.read(analyticsProvider).logPaymentView(
|
|
funnel:
|
|
isRepeat ? AnalyticsFunnel.repeat : AnalyticsFunnel.activation,
|
|
isRepeat: isRepeat,
|
|
);
|
|
});
|
|
}
|
|
|
|
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();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|