Skip to content

Commit f3d7775

Browse files
feat: Enhance ad visibility control and improve countdown timer logic
1 parent 35bfd5c commit f3d7775

4 files changed

Lines changed: 58 additions & 27 deletions

File tree

lib/modules/main/presentation/screens/main_screen.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,11 @@ class _MainScreenState extends ConsumerState<MainScreen> {
292292
);
293293

294294
default:
295-
// Show ads only when connected to VPN and ad is loaded
295+
// MainScreen controls ad visibility (Strategy Pattern approach)
296+
// Works for both GoogleAdStrategy and InternalAdStrategy
297+
// - nativeAdIsLoaded: true when ad is ready (Google or Internal)
298+
// - showCountdown: true during 60-second display window
299+
// - connected: only show when VPN is active
296300
final shouldShowAd = status == ConnectionStatus.connected &&
297301
adsState.showCountdown &&
298302
adsState.nativeAdIsLoaded;

lib/modules/main/presentation/widgets/ads/ads_state.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,9 @@ class AdsNotifier extends StateNotifier<AdsState> {
162162
}
163163

164164
/// Start the countdown timer after ad is loaded and VPN connects
165+
/// Always restarts to 60 seconds on each connection
165166
void startCountdownTimer() async {
166-
if (_countdownTimer != null && _countdownTimer!.isActive) {
167-
debugPrint('⏱️ Countdown already running, ignoring duplicate start');
168-
return;
169-
}
170-
171-
debugPrint('▶️ Starting new countdown timer');
167+
debugPrint('▶️ Starting new countdown timer (60 seconds)');
172168
_countdownTimer?.cancel();
173169

174170
// Clear any old persisted countdown before starting new one
@@ -189,10 +185,14 @@ class AdsNotifier extends StateNotifier<AdsState> {
189185
void _startCountdownFromValue(int startValue) {
190186
_countdownTimer?.cancel();
191187

188+
debugPrint('⏱️ Timer starting from $startValue seconds');
192189
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
193190
if (state.countdown > 0) {
194-
state = state.copyWith(countdown: state.countdown - 1);
191+
final newCount = state.countdown - 1;
192+
debugPrint('⏱️ Countdown: $newCount');
193+
state = state.copyWith(countdown: newCount);
195194
} else {
195+
debugPrint('⏱️ Countdown finished - hiding ad');
196196
state = state.copyWith(
197197
showCountdown: false,
198198
// Keep ad loaded - just hide it until next connection

lib/modules/main/presentation/widgets/ads/strategy/google_ad_strategy.dart

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ class GoogleAdStrategy implements AdLoadingStrategy {
6868
// Check if we need to load a new ad
6969
if (_nativeAd != null) {
7070
final adsState = ref.read(adsProvider);
71+
debugPrint('🔍 Cached ad found. State: nativeAdIsLoaded=${adsState.nativeAdIsLoaded}');
7172
if (adsState.nativeAdIsLoaded) {
7273
debugPrint('✅ Using existing valid ad');
7374
_hasInitialized = true;
7475
return;
76+
} else {
77+
debugPrint('⚠️ Cached ad exists but state says not loaded - will reload');
7578
}
7679
}
7780

@@ -98,15 +101,19 @@ class GoogleAdStrategy implements AdLoadingStrategy {
98101
);
99102
}
100103

101-
// Reset ad loaded state BEFORE disposing to prevent showing disposed ad
102-
if (_nativeAd != null) {
103-
ref.read(adsProvider.notifier).setAdLoaded(false);
104+
// Check if we already have a valid cached ad
105+
final adsState = ref.read(adsProvider);
106+
if (_nativeAd != null && adsState.nativeAdIsLoaded && !adsState.needsRefresh) {
107+
debugPrint('✅ Reusing cached ad (still fresh)');
108+
return AdLoadResult.success();
104109
}
105110

106111
_isLoading = true;
107112

108-
// Dispose previous ad if exists
113+
// Only dispose if we're reloading (ad is stale or failed)
109114
if (_nativeAd != null) {
115+
debugPrint('🔄 Disposing stale/failed ad before reload');
116+
ref.read(adsProvider.notifier).setAdLoaded(false);
110117
try {
111118
_nativeAd!.dispose();
112119
debugPrint('🗑️ Disposed previous ad');
@@ -233,7 +240,10 @@ class GoogleAdStrategy implements AdLoadingStrategy {
233240
analytics.logEvent(name: 'ad_impression', parameters: {});
234241
},
235242
),
236-
request: const AdRequest(),
243+
request: const AdRequest(
244+
keywords: ['privacy', 'security', 'technology', 'mobile', 'internet safety', 'data protection'],
245+
contentUrl: 'defyxvpn://home',
246+
),
237247
nativeTemplateStyle: templateStyle,
238248
);
239249

@@ -296,16 +306,17 @@ class GoogleAdStrategy implements AdLoadingStrategy {
296306

297307
final adsState = ref.read(adsProvider);
298308

299-
// Load ad if we don't have one or previous load failed
300-
if (_nativeAd == null || !adsState.nativeAdIsLoaded) {
309+
// Check if we need to reload (no ad, failed load, or stale)
310+
if (_nativeAd == null || !adsState.nativeAdIsLoaded || adsState.needsRefresh) {
301311
if (_isLoading) {
302312
debugPrint('⏳ Ad load already in progress...');
303313
return;
304314
}
305315

306316
debugPrint('📱 Loading ad with real IP');
307-
_hasInitialized = false;
308-
initialize(ref);
317+
loadAd(ref: ref);
318+
} else {
319+
debugPrint('✅ Keeping existing cached ad');
309320
}
310321
return;
311322
}

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
/// - Widget (this file): UI rendering, strategy selection, lifecycle management
1717
/// - Strategies: Ad loading logic, connection state handling, resource cleanup
1818
/// - State (ads_state.dart): Centralized state shared across all strategies
19+
/// - MainScreen: Controls when to show/hide ads (single source of truth)
20+
///
21+
/// **Visibility Control:**
22+
/// - MainScreen decides when to render AdsWidget (based on connection + countdown)
23+
/// - AdsWidget trusts MainScreen and renders when called (no duplicate checks)
24+
/// - This approach works cleanly with Strategy Pattern (both ad types use same state)
1925
///
2026
/// **UI Features:**
2127
/// - "ADVERTISEMENT" label (top-right corner)
@@ -79,6 +85,21 @@ class _AdsWidgetState extends ConsumerState<AdsWidget> {
7985
// Initialize strategy (this also loads the initial ad)
8086
await _strategy!.initialize(ref);
8187

88+
// Trigger rebuild to show the ad
89+
if (mounted && !_isDisposed) {
90+
setState(() {});
91+
}
92+
93+
// Check current connection state and start countdown if already connected
94+
final currentConnectionState = ref.read(connectionStateProvider).status;
95+
if (currentConnectionState == ConnectionStatus.connected) {
96+
final adsState = ref.read(adsProvider);
97+
if (adsState.nativeAdIsLoaded) {
98+
debugPrint('⏰ Already connected on init - starting countdown');
99+
ref.read(adsProvider.notifier).startCountdownTimer();
100+
}
101+
}
102+
82103
// Listen to connection changes and delegate to strategy
83104
ref.listenManual(connectionStateProvider, (previous, next) {
84105
if (_strategy == null || _isDisposed) return;
@@ -106,23 +127,18 @@ class _AdsWidgetState extends ConsumerState<AdsWidget> {
106127
Widget build(BuildContext context) {
107128
final adsState = ref.watch(adsProvider);
108129

109-
// Hide ad panel completely if loading failed (don't show errors to users)
130+
// Safety check: Hide if loading failed (MainScreen should already handle this)
110131
if (adsState.adLoadFailed) {
111132
return const SizedBox.shrink();
112133
}
113134

114-
// For internal ads: Don't show container until we have a valid image URL
115-
if (_useInternalAds) {
116-
if (adsState.customImageUrl == null || adsState.customImageUrl!.isEmpty) {
117-
return const SizedBox.shrink();
118-
}
119-
}
120-
121-
// For Google ads: Don't show container until ad is actually loaded
122-
if (!_useInternalAds && !adsState.nativeAdIsLoaded) {
135+
// Wait for strategy to initialize (happens in postFrameCallback)
136+
if (_strategy == null) {
123137
return const SizedBox.shrink();
124138
}
125139

140+
// Render ad container - MainScreen controls visibility via shouldShowAd
141+
// No need to check ad loaded state here, MainScreen already does that
126142
return SizedBox(
127143
height: 280.h,
128144
width: 336.w,

0 commit comments

Comments
 (0)