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>
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:dio/dio.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/api/api_client_provider.dart';
|
||||
import '../../../core/constants.dart';
|
||||
import '../../../core/theme/halo_tokens.dart';
|
||||
@@ -53,7 +54,21 @@ class _PaymentMethodScreenState extends ConsumerState<PaymentMethodScreen> {
|
||||
_error = null;
|
||||
});
|
||||
final api = ref.read(apiClientProvider);
|
||||
final analytics = ref.read(analyticsProvider);
|
||||
try {
|
||||
// ⭐ Capture GA4 stitching identifiers BEFORE the POST so the backend can
|
||||
// store them in product_metadata and replay them in the server-fired
|
||||
// payment_confirmed (Measurement Protocol). The backend currently
|
||||
// ignores unknown body fields — intentional; we send now, stitch later.
|
||||
final appInstanceId = await analytics.appInstanceId();
|
||||
final gaSessionId = await analytics.sessionId();
|
||||
if (!mounted) return;
|
||||
|
||||
final analyticsIds = <String, dynamic>{
|
||||
if (appInstanceId != null) 'app_instance_id': appInstanceId,
|
||||
if (gaSessionId != null) 'ga_session_id': gaSessionId,
|
||||
};
|
||||
|
||||
final body = <String, dynamic>{
|
||||
'mode': draft.mode.value,
|
||||
'duration_minutes': draft.durationMinutes,
|
||||
@@ -61,11 +76,27 @@ class _PaymentMethodScreenState extends ConsumerState<PaymentMethodScreen> {
|
||||
'is_first_session_discount': draft.isFirstSessionDiscount,
|
||||
'method': _selectedCode,
|
||||
if (draft.targetedMitraId != null) 'targeted_mitra_id': draft.targetedMitraId,
|
||||
if (analyticsIds.isNotEmpty) 'analytics': analyticsIds,
|
||||
};
|
||||
final response = await api.post('/api/client/payment-requests/', data: body);
|
||||
final data = response['data'] as Map<String, dynamic>;
|
||||
final paymentId = data['id'] as String;
|
||||
ref.read(paymentDraftNotifierProvider.notifier).setPaymentId(paymentId);
|
||||
|
||||
// ⭐ payment_started fires AFTER the id is known. A targeted mitra means
|
||||
// the returning/repeat funnel; otherwise activation.
|
||||
final isRepeat = draft.targetedMitraId != null;
|
||||
// ignore: discarded_futures
|
||||
analytics.logPaymentStarted(
|
||||
paymentRequestId: paymentId,
|
||||
amount: draft.priceIDR!,
|
||||
method: _selectedCode!,
|
||||
funnel: isRepeat ? AnalyticsFunnel.repeat : AnalyticsFunnel.activation,
|
||||
isRepeat: isRepeat,
|
||||
productType: draft.mode.value,
|
||||
durationMinutes: draft.durationMinutes!,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
context.push('/payment/waiting/$paymentId');
|
||||
} on DioException catch (e) {
|
||||
@@ -216,10 +247,20 @@ class _PaymentMethodScreenState extends ConsumerState<PaymentMethodScreen> {
|
||||
_expandedGroupIds.remove(g.id);
|
||||
}
|
||||
}),
|
||||
onSelect: (code) => setState(() {
|
||||
_selectedCode = code;
|
||||
_error = null;
|
||||
}),
|
||||
onSelect: (code) {
|
||||
// Funnel step 9 — method chosen. Fire once per pick
|
||||
// (not on every rebuild).
|
||||
if (code != _selectedCode) {
|
||||
// ignore: discarded_futures
|
||||
ref
|
||||
.read(analyticsProvider)
|
||||
.logPaymentMethodSelect(method: code);
|
||||
}
|
||||
setState(() {
|
||||
_selectedCode = code;
|
||||
_error = null;
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user