Files
halobestie-clone/client_app/lib/features/chat/screens/bestie_found_screen.dart
Ramadhan Sjamsani eeb4ea38fc feat(client/analytics): GA4 funnel instrumentation + unified home CTA
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>
2026-06-02 21:57:26 +08:00

170 lines
6.3 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/chat/active_session_notifier.dart';
import '../../../core/pairing/pairing_notifier.dart';
import '../../../core/theme/halo_tokens.dart';
import '../../payment/state/payment_draft_provider.dart';
import '../../../core/theme/widgets/widgets.dart';
/// Phase 4 Stage 5 — S9 Match-found screen.
///
/// Reskinned from the v4 mock (`v4.jsx::S9MatchV4`). Shows the matched
/// bestie's orb + a small online status dot, the matched-line copy, and a
/// primary CTA `mulai sesi {N} menit →`. The duration is read from the active
/// session payload (which the pairing notifier kicks via
/// `activeSessionProvider.refresh()` on the WS `paired` event).
///
/// `PairingActiveData` is the auto-advance signal — fired by the notifier
/// ~2s after WS `paired` lands. The same advance is also reachable manually
/// via the CTA in case the user is faster than the auto-advance timer.
class BestieFoundScreen extends ConsumerStatefulWidget {
final String sessionId;
final String mitraName;
const BestieFoundScreen({
super.key,
required this.sessionId,
required this.mitraName,
});
@override
ConsumerState<BestieFoundScreen> createState() => _BestieFoundScreenState();
}
class _BestieFoundScreenState extends ConsumerState<BestieFoundScreen> {
@override
void initState() {
super.initState();
// Funnel step 12 — paired with a mitra. A targeted-mitra draft means the
// repeat funnel; otherwise activation. Fire once on view.
final isRepeat =
ref.read(paymentDraftNotifierProvider).targetedMitraId != null;
// ignore: discarded_futures
ref.read(analyticsProvider).logPairingMatched(
funnel:
isRepeat ? AnalyticsFunnel.repeat : AnalyticsFunnel.activation,
);
ref.listenManual<PairingData>(pairingProvider, (prev, next) {
if (!mounted) return;
if (next is PairingActiveData) {
context.go('/chat/session/${next.sessionId}', extra: next.mitraName);
}
});
}
void _enterChat() {
context.go('/chat/session/${widget.sessionId}', extra: widget.mitraName);
}
@override
Widget build(BuildContext context) {
final activeSession = ref.watch(activeSessionProvider).valueOrNull;
final durationMinutes =
activeSession?.session?['duration_minutes'] as int?;
final ctaLabel = durationMinutes != null
? 'mulai sesi $durationMinutes menit →'
: 'mulai sesi →';
final subtitle = durationMinutes != null
? 'siap nemenin kamu $durationMinutes menit ke depan. cerita aja pelan-pelan ya 🤍'
: 'siap nemenin kamu. cerita aja pelan-pelan ya 🤍';
return Scaffold(
backgroundColor: HaloTokens.bg,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(
HaloSpacing.s24,
HaloSpacing.s24,
HaloSpacing.s24,
HaloSpacing.s24,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
HaloOrb(
size: 140,
seed: widget.mitraName.hashCode,
label: widget.mitraName,
),
Positioned(
right: 4,
bottom: 4,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: HaloTokens.success,
border: Border.all(
color: HaloTokens.bg,
width: 3,
),
),
),
),
],
),
const SizedBox(height: HaloSpacing.s20),
const Text(
'◦ MATCHED ◦',
style: TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.6,
color: HaloTokens.brand,
),
),
const SizedBox(height: HaloSpacing.s8),
Text(
'halo, aku bestie ${widget.mitraName}',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: HaloTokens.fontDisplay,
fontSize: 26,
height: 32 / 26,
fontWeight: FontWeight.w700,
color: HaloTokens.brandDark,
),
),
const SizedBox(height: HaloSpacing.s8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: HaloTokens.fontBody,
fontSize: 14,
height: 22 / 14,
color: HaloTokens.inkSoft,
),
),
),
],
),
),
),
HaloButton(
label: ctaLabel,
fullWidth: true,
size: HaloButtonSize.lg,
onPressed: _enterChat,
),
],
),
),
),
);
}
}