Skip to content

Commit 94b7215

Browse files
Merge branch 'dev' into release
2 parents 2f40a6a + f179c0d commit 94b7215

3 files changed

Lines changed: 135 additions & 49 deletions

File tree

lib/modules/main/presentation/widgets/google_ads.dart

Lines changed: 125 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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...");

lib/shared/services/ad_service.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ abstract interface class IAdService {
8282
Future<bool> canLoadAd();
8383

8484
/// Load ad with retry logic
85+
///
86+
/// [bypassRateLimit] - Reserved for future manual retry feature.
87+
/// When true, allows skipping the 60s rate limit for user-initiated retries.
88+
/// Currently unused as all retries are automatic and respect rate limits.
89+
/// Keep this parameter for potential Settings screen "Force Refresh" feature.
8590
Future<AdLoadResult> loadAdWithRetry({
8691
required String adUnitId,
8792
required NativeAdListener listener,
@@ -94,6 +99,9 @@ abstract interface class IAdService {
9499
Future<void> logAdEvent(String eventName, Map<String, dynamic> parameters);
95100

96101
/// Check rate limiting
102+
///
103+
/// [bypassRateLimit] - When true, skip rate limit check.
104+
/// Used for manual user-initiated ad refresh (future feature).
97105
bool canMakeRequest({bool bypassRateLimit = false});
98106

99107
/// Get time until rate limit reset

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: defyx_vpn
22
description: "A new Vpn app"
33
publish_to: "none"
4-
version: 4.4.3+245
4+
version: 4.4.4+246
55

66
environment:
77
sdk: ^3.5.3
@@ -104,7 +104,7 @@ msix_config:
104104
display_name: Defyx VPN
105105
publisher_display_name: UnboundTech UG
106106
identity_name: UnboundTechUG.6065AEC5A207
107-
msix_version: 4.4.3.0
107+
msix_version: 4.4.4.0
108108
publisher: "CN=62937938-2AE2-4C12-9ED8-4C418C0CADF2"
109109
logo_path: assets/images/logo.png
110110
capabilities: runFullTrust,internetClientServer,privateNetworkClientServer,unvirtualizedResources

0 commit comments

Comments
 (0)