1- import 'dart:io' ;
2-
31import 'package:defyx_vpn/app/ad_director_provider.dart' ;
42import 'package:defyx_vpn/app/router/app_router.dart' ;
53import 'package:defyx_vpn/core/theme/app_theme.dart' ;
64import 'package:defyx_vpn/modules/core/vpn.dart' ;
75import 'package:defyx_vpn/modules/core/desktop_platform_handler.dart' ;
86import 'package:defyx_vpn/modules/main/presentation/widgets/ump_service.dart' ;
97import 'package:defyx_vpn/shared/providers/language_provider.dart' ;
10- import 'package:defyx_vpn/shared/providers/ad_personalization_provider .dart' ;
8+ import 'package:defyx_vpn/shared/providers/ad_readiness_coordinator .dart' ;
119import 'package:defyx_vpn/shared/providers/connection_state_provider.dart' ;
1210import 'package:flutter/foundation.dart' ;
1311import 'package:flutter/material.dart' ;
1412import 'package:flutter_screenutil/flutter_screenutil.dart' ;
1513import 'package:flutter_riverpod/flutter_riverpod.dart' ;
16- import 'package:google_mobile_ads/google_mobile_ads.dart' ;
1714import 'package:defyx_vpn/shared/services/animation_service.dart' ;
1815import 'package:defyx_vpn/shared/services/alert_service.dart' ;
1916import 'package:toastification/toastification.dart' ;
@@ -28,36 +25,42 @@ class App extends ConsumerWidget {
2825 @override
2926 Widget build (BuildContext context, WidgetRef ref) {
3027 // Eagerly trigger environment computation
31- ref.read (adEnvironmentProvider);
32-
33- // Listen for VPN profile setup to trigger AdMob initialization
34- ref.listen <AdPersonalizationState >(adPersonalizationProvider, (
35- previous,
36- next,
37- ) {
38- // When VPN profile is setup, trigger AdMob initialization
39- if (next.vpnProfileSetup && (previous? .vpnProfileSetup != true )) {
40- debugPrint (
41- '🚀 VPN profile setup detected - triggering AdMob initialization' ,
42- );
43- _handleAdConfiguration (ref);
28+ final environmentAsync = ref.watch (adEnvironmentProvider);
29+
30+ // Single listener for ad readiness state changes
31+ ref.listen (adReadinessCoordinatorProvider, (previous, next) {
32+ if (previous == null ) return ;
33+
34+ // When canInitializeAdMob transitions to true, start the flow
35+ if (next.canInitializeAdMob && ! previous.canInitializeAdMob) {
36+ debugPrint ('🚀 Privacy accepted - starting ad initialization flow' );
37+
38+ environmentAsync.whenData ((environment) {
39+ if (environment.shouldInitializeAdMob) {
40+ _initializeAdFlow (ref);
41+ } else {
42+ debugPrint (
43+ '📱 Using internal ads only (${environment .isIranian ? "Iranian user" : "desktop platform" })' ,
44+ );
45+ }
46+ });
47+ }
48+
49+ // When consent completes and we're disconnected, retry ad load
50+ if (next.canLoadAds && ! previous.canLoadAds) {
51+ final connectionState = ref.read (connectionStateProvider).status;
52+ if (connectionState == ConnectionStatus .disconnected) {
53+ debugPrint ('✅ Consent complete & disconnected - triggering ad load' );
54+ Future .delayed (const Duration (milliseconds: 100 ), () {
55+ ref.read (adStrategyManagerProvider)? .retryGoogleAdLoad ();
56+ });
57+ }
4458 }
4559 });
4660
4761 return FutureBuilder <void >(
4862 future: _initializeApp (ref),
49- builder: (context, snapshot) {
50- // Check immediately on build
51- _handleAdConfiguration (ref);
52-
53- // Also check after frame to ensure persisted state is loaded
54- WidgetsBinding .instance.addPostFrameCallback ((_) {
55- debugPrint ('📱 Post-frame callback - rechecking AdMob initialization' );
56- _handleAdConfiguration (ref);
57- });
58-
59- return _buildApp (context, ref);
60- },
63+ builder: (context, snapshot) => _buildApp (context, ref),
6164 );
6265 }
6366
@@ -67,96 +70,28 @@ class App extends ConsumerWidget {
6770 await AnimationService ().init ();
6871 }
6972
70- void _handleAdConfiguration (WidgetRef ref) {
71- // Use adEnvironmentProvider to decide AdMob initialization
72- final environmentAsync = ref.read (adEnvironmentProvider);
73-
74- environmentAsync.whenData ((environment) {
75- if (! environment.shouldInitializeAdMob) {
76- debugPrint (
77- '📱 Using internal ads only (${environment .isIranian ? "Iranian user" : "desktop platform" })' ,
78- );
79- } else {
80- // Only initialize AdMob after VPN profile is setup (privacy notice accepted)
81- final adState = ref.read (adPersonalizationProvider);
82- debugPrint (
83- '🔍 AdMob check: vpnProfileSetup=${adState .vpnProfileSetup }, adMobInitializationStarted=${adState .adMobInitializationStarted }' ,
84- );
85-
86- if (adState.vpnProfileSetup && ! adState.adMobInitializationStarted) {
87- debugPrint ('📱 VPN profile ready - Initializing AdMob' );
88-
89- // Defer state modification to avoid modifying provider during build
90- Future .microtask (() {
91- ref
92- .read (adPersonalizationProvider.notifier)
93- .markAdMobInitializationStarted ();
94- _initializeMobileAdsWithConsent (ref, environment);
95- });
96- } else if (! adState.vpnProfileSetup) {
97- debugPrint (
98- '⏳ Waiting for VPN profile setup before initializing AdMob' ,
73+ /// Initialize ad flow using the coordinator
74+ void _initializeAdFlow (WidgetRef ref) {
75+ final coordinator = ref.read (adReadinessCoordinatorProvider.notifier);
76+ final umpService = ref.read (umpServiceProvider);
77+
78+ coordinator.initializeAdFlow (
79+ onRunUMP: (shouldRequestUMP) async {
80+ if (shouldRequestUMP) {
81+ debugPrint ('🔐 Running UMP consent flow...' );
82+ await umpService.requestConsentWithATT (
83+ ref: ref,
84+ onDone: () {
85+ debugPrint ('✅ UMP flow complete - marking consent done' );
86+ coordinator.markConsentComplete ();
87+ },
9988 );
100- } else if (adState.adMobInitializationStarted) {
101- debugPrint ('✅ AdMob already initialized, skipping' );
89+ } else {
90+ debugPrint ('⏭️ Skipping UMP (ATT denied/restricted)' );
91+ coordinator.markConsentComplete ();
10292 }
103- }
104- });
105- }
106-
107- Future <void > _initializeMobileAdsWithConsent (
108- WidgetRef ref,
109- AdEnvironment environment,
110- ) async {
111- try {
112- // Environment already verified shouldInitializeAdMob = true
113- debugPrint ('📱 Starting AdMob initialization...' );
114-
115- if (Platform .isAndroid || Platform .isIOS) {
116- // Request App Tracking Transparency (iOS only)
117- if (Platform .isIOS) {
118- // Small delay to ensure UI is ready
119- await Future .delayed (const Duration (milliseconds: 500 ));
120-
121- // Request ATT authorization - this shows dialog and stores result
122- final status = await ref
123- .read (adPersonalizationProvider.notifier)
124- .requestATT ();
125- debugPrint ('📱 ATT dialog result: ${status .name }' );
126- }
127-
128- // UMP service will handle ATT status and decide whether to show consent
129- final umpService = ref.read (umpServiceProvider);
130- await umpService.requestConsentWithATT (
131- ref: ref,
132- onDone: () async {
133- // Initialize Mobile Ads after consent flow completes
134- await MobileAds .instance.initialize ();
135- debugPrint ('📱 Google AdMob initialized' );
136-
137- // Mark consent flow as complete - NOW SAFE TO LOAD ADS
138- ref
139- .read (adPersonalizationProvider.notifier)
140- .markConsentFlowComplete ();
141-
142- // Trigger ad loading retry if still disconnected
143- final connectionState = ref.read (connectionStateProvider).status;
144- if (connectionState == ConnectionStatus .disconnected) {
145- debugPrint (
146- '🔄 Consent complete & disconnected - triggering ad load retry' ,
147- );
148- // Small delay to ensure state propagation
149- await Future .delayed (const Duration (milliseconds: 100 ));
150-
151- final manager = ref.read (adStrategyManagerProvider);
152- manager? .retryGoogleAdLoad ();
153- }
154- },
155- );
156- }
157- } catch (error) {
158- debugPrint ('Error initializing Google AdMob: $error ' );
159- }
93+ },
94+ );
16095 }
16196
16297 Widget _buildApp (BuildContext context, WidgetRef ref) {
0 commit comments