@@ -234,16 +234,26 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
234234 return ;
235235 }
236236
237- // Load fresh ad if none exists or previous was invalid
237+ // CRITICAL: Only load fresh ad if VPN is DISCONNECTED (real IP guaranteed)
238238 if (! _hasInitialized) {
239- // Check cache metadata to decide whether to load immediately or delay
240- _checkCacheAndLoad ();
239+ final connectionState = ref.read (connectionStateProvider);
240+
241+ if (connectionState.status == ConnectionStatus .connected) {
242+ // VPN is connected - user's real IP is masked!
243+ // Don't load ad now - wait for disconnect to ensure proper geographic targeting
244+ debugPrint ('🚫 VPN connected on app start - waiting for disconnect to load ad with real IP' );
245+ debugPrint ('⚠️ Loading ads with VPN IP would cause wrong geographic targeting and lower eCPM' );
246+ // Ad will load automatically when user disconnects (connection listener below)
247+ } else {
248+ // VPN disconnected - real IP visible - safe to load with proper targeting
249+ debugPrint ('✅ VPN disconnected - loading ad with real IP for accurate targeting' );
250+ _checkCacheAndLoad ();
251+ }
241252 }
242253 });
243254
244255 // Smart ad refresh & countdown management
245256 ref.listenManual (connectionStateProvider, (previous, next) {
246- final adsState = ref.read (googleAdsProvider);
247257 final refreshStrategy = ref.read (adRefreshStrategyProvider);
248258
249259 // Record connection for activity tracking
@@ -252,46 +262,74 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
252262 refreshStrategy.recordConnection ();
253263 }
254264
255- // When disconnected, check if ad needs refresh using adaptive strategy
265+ // When disconnected, load ad with real IP if we don't have one yet
256266 if (next.status == ConnectionStatus .disconnected &&
257- previous? .status == ConnectionStatus .connected &&
258- adsState.nativeAdIsLoaded &&
259- refreshStrategy.shouldRefreshAd () &&
260- ! _isDisposed) {
267+ previous? .status == ConnectionStatus .connected) {
268+ debugPrint ('🔌 Disconnected - hiding ad' );
261269
262- // Check 60s throttle to comply with AdMob policy
263- final now = DateTime .now ();
264- if (_lastAdRequest != null ) {
265- final timeSinceLastRequest = now.difference (_lastAdRequest! );
266- if (timeSinceLastRequest.inSeconds < 60 ) {
267- debugPrint ('⏱️ Skipping ad refresh - only ${timeSinceLastRequest .inSeconds }s since last request' );
268- return ;
269- }
270+ // CRITICAL: If no ad loaded yet (app opened while connected), load one now with real IP
271+ if (_nativeAd == null && ! _hasInitialized && ! _isDisposed) {
272+ debugPrint ('📱 No ad loaded yet - loading now with real IP for proper targeting' );
273+ _checkCacheAndLoad ();
274+ } else if (_nativeAd != null ) {
275+ debugPrint ('💾 Keeping existing ad in memory for reuse' );
270276 }
271-
272- debugPrint ('🔄 Ad refresh triggered by adaptive strategy' );
273- _lastAdRequest = now;
274- _hasInitialized = false ;
275- _initializeAds ();
276277 return ;
277278 }
278279
279- // Start countdown when connected (reuse existing ad)
280+ // When connecting, check if we need to refresh the ad BEFORE showing it
280281 if (next.status == ConnectionStatus .connected &&
281282 previous? .status != ConnectionStatus .connected &&
282- adsState.nativeAdIsLoaded &&
283283 ! _isDisposed) {
284- debugPrint ('▶️ Starting 60s countdown for ad impression' );
285- ref.read (googleAdsProvider.notifier).startCountdownTimer ();
284+
285+ final adsState = ref.read (googleAdsProvider);
286+
287+ // CRITICAL: Don't show ad or refresh if no ad loaded (user needs to disconnect first)
288+ if (! adsState.nativeAdIsLoaded) {
289+ debugPrint ('⚠️ No ad available - user needs to disconnect VPN first to load with real IP' );
290+ return ;
291+ }
292+
293+ // Check if ad needs refresh based on adaptive strategy
294+ if (adsState.nativeAdIsLoaded && refreshStrategy.shouldRefreshAd ()) {
295+ // Check 60s throttle to comply with AdMob policy
296+ final now = DateTime .now ();
297+ if (_lastAdRequest != null ) {
298+ final timeSinceLastRequest = now.difference (_lastAdRequest! );
299+ if (timeSinceLastRequest.inSeconds < 60 ) {
300+ debugPrint ('⏱️ Ad needs refresh but rate limit active - reusing existing ad' );
301+ } else {
302+ debugPrint ('🔄 Ad refresh triggered before showing (adaptive strategy)' );
303+ _lastAdRequest = now;
304+ _hasInitialized = false ;
305+ _initializeAds ();
306+ return ;
307+ }
308+ } else {
309+ debugPrint ('🔄 Ad refresh triggered before showing (adaptive strategy)' );
310+ _lastAdRequest = DateTime .now ();
311+ _hasInitialized = false ;
312+ _initializeAds ();
313+ return ;
314+ }
315+ }
316+
317+ // Start countdown for existing ad
318+ if (adsState.nativeAdIsLoaded) {
319+ debugPrint ('▶️ Starting 60s countdown for ad impression' );
320+ ref.read (googleAdsProvider.notifier).startCountdownTimer ();
321+ }
286322 }
287323 });
288324 }
289325
326+ /// Initialize ads based on platform and user location
327+ ///
328+ /// [bypassRateLimit] - Reserved for future manual refresh feature.
329+ /// Currently unused as all refreshes are automatic and respect rate limits.
290330 void _initializeAds ({bool bypassRateLimit = false }) async {
291331 if (_isDisposed || _hasInitialized) return ;
292332
293- _hasInitialized = true ;
294-
295333 try {
296334 // Disable Google Ads on non-mobile platforms.
297335 if (! (Platform .isAndroid || Platform .isIOS)) {
@@ -311,24 +349,21 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
311349 ref.read (shouldShowGoogleAdsProvider.notifier).state = shouldShowGoogle;
312350
313351 if (shouldShowGoogle) {
314- // Check if static ad already exists and is still valid
315- final adsState = ref.read (googleAdsProvider);
316- if (_nativeAd != null && adsState.nativeAdIsLoaded && ! adsState.needsRefresh) {
317- // Verify ad is actually valid before reusing
318- try {
319- // ignore: unnecessary_null_checks
320- final _ = _nativeAd! .hashCode;
321- debugPrint ('♻️ Reusing existing loaded ad (age: ${DateTime .now ().difference (adsState .adLoadedAt !).inMinutes }min)' );
322- return ;
323- } catch (e) {
324- debugPrint ('⚠️ Existing ad became invalid - loading fresh ad' );
325- _nativeAd = null ;
326- ref.read (googleAdsProvider.notifier).resetState ();
327- }
352+ // Consolidated ad validation - check if we need to load a new ad
353+ if (! _shouldLoadNewAd ()) {
354+ debugPrint ('✅ Using existing valid ad' );
355+ _hasInitialized = true ;
356+ return ;
328357 }
358+
359+ // Mark as initialized before starting load process
360+ _hasInitialized = true ;
329361 _loadGoogleAd (bypassRateLimit: bypassRateLimit);
330362 return ;
331363 }
364+
365+ // Mark as initialized for non-Google ads too
366+ _hasInitialized = true ;
332367
333368 final customAdData = await AdvertiseDirector .getRandomCustomAd (ref);
334369 if (! _isDisposed) {
@@ -493,6 +528,40 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
493528 }
494529 }
495530
531+ /// Consolidated check: Should we load a new ad?
532+ /// Returns false if existing ad is valid and doesn't need refresh
533+ bool _shouldLoadNewAd () {
534+ // No existing ad - need to load
535+ if (_nativeAd == null ) return true ;
536+
537+ final adsState = ref.read (googleAdsProvider);
538+
539+ // Ad not loaded in state - need to load
540+ if (! adsState.nativeAdIsLoaded) return true ;
541+
542+ // Verify ad is actually valid (not disposed)
543+ try {
544+ // ignore: unnecessary_null_checks
545+ final _ = _nativeAd! .hashCode;
546+ } catch (e) {
547+ debugPrint ('⚠️ Existing ad became invalid - need fresh ad' );
548+ _nativeAd = null ;
549+ ref.read (googleAdsProvider.notifier).resetState ();
550+ return true ;
551+ }
552+
553+ // Ad exists and is valid - check if it needs refresh due to age
554+ if (adsState.needsRefresh) {
555+ debugPrint ('📅 Ad is stale (>15 min) - need fresh ad' );
556+ return true ;
557+ }
558+
559+ // Ad is valid and fresh - reuse it
560+ final ageMinutes = DateTime .now ().difference (adsState.adLoadedAt! ).inMinutes;
561+ debugPrint ('♻️ Existing ad is valid (age: ${ageMinutes }min) - reusing' );
562+ return false ;
563+ }
564+
496565 Future <void > _checkCacheAndLoad () async {
497566 if (_isDisposed) return ;
498567
@@ -505,12 +574,18 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
505574 final timeSinceError = DateTime .now ().difference (metadata.loadedAt);
506575
507576 // If we recently had a "no fill" error (code 3), delay the next attempt
577+ // Linear decay: 2 minutes delay if error just happened, 0 delay after 5 minutes
508578 if (metadata.lastErrorCode == '3' && timeSinceError.inSeconds < 300 ) {
509- // Had no-fill error less than 5 minutes ago - delay loading
510- final delaySeconds = 120 - timeSinceError.inSeconds;
511- if (delaySeconds > 0 ) {
512- debugPrint ('📊 Cache shows recent no-fill error - delaying load by ${delaySeconds }s' );
513- await Future .delayed (Duration (seconds: delaySeconds));
579+ const maxDelaySeconds = 120 ; // 2 minutes max
580+ const errorWindowSeconds = 300 ; // 5 minute window
581+
582+ // Calculate remaining delay: decreases linearly from 120s to 0s over 5 minutes
583+ final elapsedSeconds = timeSinceError.inSeconds;
584+ final remainingDelay = ((errorWindowSeconds - elapsedSeconds) / errorWindowSeconds * maxDelaySeconds).round ();
585+
586+ if (remainingDelay > 0 ) {
587+ debugPrint ('📊 Recent no-fill error (${timeSinceError .inMinutes }m ago) - waiting ${remainingDelay }s before retry' );
588+ await Future .delayed (Duration (seconds: remainingDelay));
514589 }
515590 }
516591 }
@@ -702,6 +777,9 @@ class _GoogleAdsState extends ConsumerState<GoogleAds> {
702777 // Silent auto-retry in background - just show loading state
703778 debugPrint ('🔄 Ad failed - auto-retry running in background' );
704779 return _buildLoadingWidget ("Loading ads..." );
780+ } else if (! _hasInitialized && ref.read (connectionStateProvider).status == ConnectionStatus .connected) {
781+ // App opened while VPN connected - waiting for disconnect to load with real IP
782+ return _buildLoadingWidget ("Disconnect VPN to load ads..." );
705783 } else {
706784 // Initial load state
707785 return _buildLoadingWidget ("Preparing ads..." );
0 commit comments