Skip to content

Commit 6bc9f08

Browse files
Merge branch 'dev' into release
2 parents 91c819f + 6c1d427 commit 6bc9f08

11 files changed

Lines changed: 601 additions & 267 deletions

File tree

lib/app/advertise_director.dart

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:io';
12
import 'package:defyx_vpn/core/data/local/secure_storage/secure_storage.dart';
23
import 'package:defyx_vpn/core/data/local/secure_storage/secure_storage_const.dart';
34
import 'package:flutter_timezone/flutter_timezone.dart';
@@ -11,24 +12,34 @@ class AdvertiseDirector {
1112
AdvertiseDirector(this.ref);
1213

1314
static Future<bool> shouldUseInternalAds(WidgetRef ref) async {
15+
// STRATEGY SELECTION (for backward compatibility with desktop):
16+
// - Desktop (Windows/macOS/Linux) → InternalAdStrategy only (no AdMob support)
17+
// - Mobile (Android/iOS) → DUAL strategy approach:
18+
// * GoogleAdStrategy handles AdMob ads (disconnected state ONLY)
19+
// * InternalAdStrategy handles internal ads (connected state ONLY)
20+
// * AdsWidget coordinates between the two strategies
21+
22+
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
23+
debugPrint('📍 Ad Manager - Desktop platform detected, using InternalAdStrategy only');
24+
return true;
25+
}
26+
27+
// Mobile platforms use DUAL strategy (GoogleAdStrategy + InternalAdStrategy)
28+
// AdsWidget automatically initializes both and routes based on connection state:
29+
// - When CONNECTED: InternalAdStrategy shows internal ads (timezone-specific or General)
30+
// - When DISCONNECTED: GoogleAdStrategy shows AdMob ads
31+
debugPrint('📍 Ad Manager - Mobile platform detected, using DUAL strategy approach');
32+
1433
final String currentTimeZone = await FlutterTimezone.getLocalTimezone();
1534
debugPrint('📍 Ad Manager - Current Timezone: $currentTimeZone');
16-
17-
final adversies =
18-
await ref.read(secureStorageProvider).readMap(apiAvertiseKey);
19-
35+
36+
final adversies = await ref.read(secureStorageProvider).readMap(apiAvertiseKey);
2037
if (adversies['api_advertise'] != null) {
2138
final advertiseMap = adversies['api_advertise'] as Map<String, dynamic>;
22-
final hasInternalAds = advertiseMap.containsKey(currentTimeZone);
23-
debugPrint('📍 Ad Manager - Has internal ads for timezone: $hasInternalAds');
24-
if (hasInternalAds) {
25-
debugPrint('📍 Ad Manager - Available timezones: ${advertiseMap.keys.toList()}');
26-
}
27-
return hasInternalAds;
39+
debugPrint('📍 Ad Manager - Available ad keys: ${advertiseMap.keys.toList()}');
2840
}
2941

30-
debugPrint('📍 Ad Manager - No advertise data found');
31-
return false;
42+
return false; // Mobile uses dual strategy (both GoogleAdStrategy + InternalAdStrategy)
3243
}
3344

3445
static Future<String> getCustomAdBanner(WidgetRef ref) async {
@@ -50,26 +61,47 @@ class AdvertiseDirector {
5061

5162
if (adversies['api_advertise'] != null) {
5263
final advertiseMap = adversies['api_advertise'] as Map<String, dynamic>;
64+
65+
// Try timezone-specific ads first
5366
if (advertiseMap.containsKey(currentTimeZone)) {
5467
final adsData = advertiseMap[currentTimeZone] as List<dynamic>;
55-
debugPrint('📍 Ad Manager - Found ${adsData.length} ads for timezone');
68+
debugPrint('📍 Ad Manager - Found ${adsData.length} timezone-specific ads');
69+
if (adsData.isNotEmpty) {
70+
final random = Random();
71+
final randomIndex = random.nextInt(adsData.length);
72+
final selectedAd = adsData[randomIndex] as List<dynamic>;
73+
74+
if (selectedAd.length >= 2) {
75+
debugPrint('📍 Ad Manager - Selected timezone ad #$randomIndex');
76+
return {
77+
'imageUrl': selectedAd[0] as String,
78+
'clickUrl': selectedAd[1] as String,
79+
};
80+
}
81+
}
82+
}
83+
84+
// Fallback to "General" ads if no timezone-specific ads
85+
if (advertiseMap.containsKey('General')) {
86+
final adsData = advertiseMap['General'] as List<dynamic>;
87+
debugPrint('📍 Ad Manager - Using "General" fallback ads (${adsData.length} available)');
5688
if (adsData.isNotEmpty) {
5789
final random = Random();
5890
final randomIndex = random.nextInt(adsData.length);
5991
final selectedAd = adsData[randomIndex] as List<dynamic>;
6092

6193
if (selectedAd.length >= 2) {
62-
debugPrint('📍 Ad Manager - Selected ad #$randomIndex');
94+
debugPrint('📍 Ad Manager - Selected General ad #$randomIndex');
6395
return {
6496
'imageUrl': selectedAd[0] as String,
6597
'clickUrl': selectedAd[1] as String,
6698
};
6799
}
68100
}
69-
} else {
70-
debugPrint('📍 Ad Manager - No ads for timezone: $currentTimeZone');
71-
debugPrint('📍 Ad Manager - Available timezones: ${advertiseMap.keys.toList()}');
72101
}
102+
103+
debugPrint('📍 Ad Manager - No ads for timezone: $currentTimeZone');
104+
debugPrint('📍 Ad Manager - Available keys: ${advertiseMap.keys.toList()}');
73105
}
74106

75107
debugPrint('📍 Ad Manager - Returning empty ad');

lib/app/app.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ class App extends ConsumerWidget {
4747

4848
final shouldUseInternalAds = snapshot.data!;
4949
if (shouldUseInternalAds) {
50-
debugPrint('Using internal ads');
50+
debugPrint('📱 Using internal ads (desktop platform or backend config)');
5151
} else {
52-
_initializeMobileAdsWithConsent(ref);
52+
// Only initialize AdMob on mobile platforms
53+
if (Platform.isAndroid || Platform.isIOS) {
54+
_initializeMobileAdsWithConsent(ref);
55+
} else {
56+
debugPrint('📱 Skipping AdMob initialization on desktop platform');
57+
}
5358
}
5459
}
5560

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

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -213,27 +213,11 @@ class _MainScreenState extends ConsumerState<MainScreen> {
213213
onSecretTap: _handleSecretTap,
214214
onPingRefresh: _logic.refreshPing,
215215
),
216-
SizedBox(
217-
height: connectionState.status ==
218-
ConnectionStatus.connected
219-
? 40.h
220-
: 80.h,
221-
),
222-
SizedBox(
223-
height: connectionState.status ==
224-
ConnectionStatus.connected
225-
? 0.24.sh
226-
: 0.28.sh,
227-
),
216+
SizedBox(height: 50.h), // Reduced to raise ads higher
217+
SizedBox(height: 0.16.sh), // Reduced to raise ads higher
228218
_buildContentSection(
229219
connectionState.status, adsState),
230-
SizedBox(
231-
height: connectionState.status ==
232-
ConnectionStatus.connected &&
233-
adsState.showCountdown
234-
? 140.h
235-
: 0.15.sh,
236-
),
220+
SizedBox(height: 0.15.sh), // Consistent bottom spacing
237221
],
238222
),
239223
],
@@ -275,58 +259,89 @@ class _MainScreenState extends ConsumerState<MainScreen> {
275259
}
276260

277261
Widget _buildContentSection(ConnectionStatus status, dynamic adsState) {
262+
debugPrint('🎨 _buildContentSection called:');
263+
debugPrint(' Status: ${status.name}');
264+
debugPrint(' nativeAdIsLoaded: ${adsState.nativeAdIsLoaded}');
265+
debugPrint(' showCountdown: ${adsState.showCountdown}');
266+
debugPrint(' adLoadFailed: ${adsState.adLoadFailed}');
267+
268+
// Determine if we should show ads based on state
269+
bool shouldShowAd = false;
270+
Widget? mainContent;
271+
278272
switch (status) {
279273
case ConnectionStatus.noInternet:
280274
_dinoGame ??= DinoGame();
281-
return SizedBox(
275+
mainContent = SizedBox(
282276
height: 200.h,
283277
child: ClipRRect(
284278
borderRadius: BorderRadius.circular(16.r),
285279
child: GameWidget(game: _dinoGame!),
286280
),
287281
);
282+
break;
288283

289284
case ConnectionStatus.disconnected:
290-
return TipsSliderSection(
291-
status: status,
292-
);
285+
// DISCONNECTED: Show AdMob ad (after VPN use) if available, else show tips
286+
final shouldShowAdMobAd = adsState.showCountdown &&
287+
adsState.nativeAdIsLoaded;
288+
289+
debugPrint(' 🎯 DISCONNECTED: shouldShowAdMobAd=$shouldShowAdMobAd');
290+
291+
if (shouldShowAdMobAd) {
292+
debugPrint(' ✅ Rendering AdMob ad');
293+
shouldShowAd = true;
294+
} else {
295+
debugPrint(' ℹ️ Rendering tips slider');
296+
mainContent = TipsSliderSection(status: status);
297+
}
298+
break;
293299

294300
default:
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
300-
final shouldShowAd = status == ConnectionStatus.connected &&
301-
adsState.showCountdown &&
302-
adsState.nativeAdIsLoaded;
303-
304-
return SizedBox(
305-
height: 280.h,
306-
width: 336.w,
307-
child: AnimatedSlide(
308-
offset: Offset(
309-
0,
310-
shouldShowAd ? 0.0 : 1.0,
311-
),
312-
duration: _animationService
313-
.adjustDuration(const Duration(milliseconds: 800)),
314-
curve: Curves.easeOut,
315-
child: AnimatedOpacity(
316-
opacity: shouldShowAd ? 1.0 : 0.0,
317-
duration: _animationService
318-
.adjustDuration(const Duration(milliseconds: 500)),
319-
curve: Curves.easeInOut,
320-
child: DecoratedBox(
321-
decoration: BoxDecoration(
322-
color: const Color(0xFF19312F),
323-
borderRadius: BorderRadius.circular(10.r),
301+
// CONNECTED/CONNECTING: Show internal ad (during VPN use) if available
302+
final shouldShowInternalAd = adsState.showCountdown &&
303+
(adsState.customImageUrl?.isNotEmpty ?? false);
304+
305+
debugPrint(' 🎯 CONNECTED/OTHER: shouldShowInternalAd=$shouldShowInternalAd (hasCustomImage=${adsState.customImageUrl != null})');
306+
307+
if (shouldShowInternalAd) {
308+
debugPrint(' ✅ Rendering internal ad');
309+
shouldShowAd = true;
310+
} else {
311+
debugPrint(' ⚪ Rendering empty (no ad)');
312+
mainContent = const SizedBox.shrink();
313+
}
314+
}
315+
316+
// CRITICAL: Keep AdsWidget in ONE position in the tree to prevent dispose/recreate cycles
317+
// Control visibility with Opacity instead of moving widget around
318+
return Stack(
319+
alignment: Alignment.topCenter, // Align to top-center for consistent positioning
320+
children: [
321+
// Always in tree at same position - never recreated
322+
Opacity(
323+
opacity: shouldShowAd ? 1.0 : 0.0,
324+
child: IgnorePointer(
325+
ignoring: !shouldShowAd, // Prevent touch events when hidden
326+
child: Padding(
327+
padding: EdgeInsets.only(top: 50.h), // Match the spacing above ads
328+
child: SizedBox(
329+
height: 280.h,
330+
width: 336.w,
331+
child: DecoratedBox(
332+
decoration: BoxDecoration(
333+
color: const Color(0xFF19312F),
334+
borderRadius: BorderRadius.circular(10.r),
335+
),
336+
child: _adsWidget, // Always same widget instance, same position
324337
),
325-
child: _adsWidget,
326338
),
327339
),
328340
),
329-
);
330-
}
341+
),
342+
// Show main content when not showing ad
343+
if (!shouldShowAd && mainContent != null) mainContent,
344+
],
345+
);
331346
}
332347
}

0 commit comments

Comments
 (0)