Skip to content

Commit e35e7bb

Browse files
Merge branch 'dev' into release
# Conflicts: # pubspec.yaml
2 parents 0dcb389 + f3d7775 commit e35e7bb

9 files changed

Lines changed: 368 additions & 41 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
android:label="DefyxVPN"
2828
android:icon="@mipmap/ic_launcher"
2929
android:banner="@drawable/tv_banner"
30+
android:usesCleartextTraffic="true"
3031
tools:ignore="ForegroundServicePermission">
3132

3233
<receiver

ios/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<key>NSVPNConfigurationUsageDescription</key>
3434
<string>Defyx needs access to VPN configurations to secure your connection.</string>
3535
<key>NSUserTrackingUsageDescription</key>
36-
<string>We show personalized ads to support this free VPN service. Your data stays private and secure.</string>
36+
<string>We need permission to show you personalized ads. This helps support our free VPN service. Declining means you'll see generic ads instead.</string>
3737
<key>NSPhotoLibraryUsageDescription</key>
3838
<string>Defyx needs access to your photo library to let you select and share images, such as profile pictures or VPN connection QR codes.</string>
3939
<key>UIApplicationSupportsIndirectInputEvents</key>

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,

lib/shared/layout/navbar/widgets/custom_webview_screen.dart

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
44
import 'package:flutter_screenutil/flutter_screenutil.dart';
55
import 'package:url_launcher/url_launcher.dart';
66
import 'package:webview_flutter/webview_flutter.dart';
7+
import 'package:webview_flutter_android/webview_flutter_android.dart';
8+
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
79

810
class CustomWebViewScreen extends StatefulWidget {
911
final String url;
@@ -20,7 +22,7 @@ class CustomWebViewScreen extends StatefulWidget {
2022
}
2123

2224
class _CustomWebViewScreenState extends State<CustomWebViewScreen> {
23-
late final WebViewController _controller;
25+
WebViewController? _controller;
2426
bool _isLoading = true;
2527

2628
@override
@@ -32,23 +34,69 @@ class _CustomWebViewScreenState extends State<CustomWebViewScreen> {
3234
return;
3335
}
3436

35-
_controller = WebViewController()
37+
_initializeWebView();
38+
}
39+
40+
Future<void> _initializeWebView() async {
41+
// Clear all cookies before loading to prevent tracking
42+
await WebViewCookieManager().clearCookies();
43+
44+
// Use platform-specific params for native cookie blocking
45+
late final PlatformWebViewControllerCreationParams params;
46+
47+
if (Platform.isIOS) {
48+
// iOS: Use WKWebView which blocks 3rd-party cookies by default (iOS 14+)
49+
params = WebKitWebViewControllerCreationParams(
50+
allowsInlineMediaPlayback: true,
51+
mediaTypesRequiringUserAction: const {},
52+
);
53+
} else if (Platform.isAndroid) {
54+
// Android: Use native WebView with built-in privacy features
55+
params = AndroidWebViewControllerCreationParams();
56+
} else {
57+
params = const PlatformWebViewControllerCreationParams();
58+
}
59+
60+
final controller = WebViewController.fromPlatformCreationParams(params)
3661
..setJavaScriptMode(JavaScriptMode.unrestricted)
3762
..setNavigationDelegate(
3863
NavigationDelegate(
39-
onPageStarted: (String url) {
40-
setState(() {
41-
_isLoading = true;
42-
});
64+
onNavigationRequest: (NavigationRequest request) {
65+
// Clear cookies before every navigation to prevent tracking persistence
66+
WebViewCookieManager().clearCookies();
67+
debugPrint('🍪 Cookies cleared before navigation to: ${request.url}');
68+
return NavigationDecision.navigate;
69+
},
70+
onPageStarted: (String url) async {
71+
// Clear cookies when page starts loading as an additional safeguard
72+
await WebViewCookieManager().clearCookies();
73+
if (mounted) {
74+
setState(() {
75+
_isLoading = true;
76+
});
77+
}
4378
},
4479
onPageFinished: (String url) {
45-
setState(() {
46-
_isLoading = false;
47-
});
80+
if (mounted) {
81+
setState(() {
82+
_isLoading = false;
83+
});
84+
}
4885
},
4986
),
5087
)
51-
..loadRequest(Uri.parse(widget.url));
88+
..setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 DNT/1');
89+
90+
// Load the URL
91+
await controller.loadRequest(Uri.parse(widget.url));
92+
93+
_controller = controller;
94+
95+
if (mounted) {
96+
setState(() {});
97+
}
98+
99+
debugPrint('🍪 WebView initialized: Native privacy + continuous cookie blocking');
52100
}
53101

54102
Future<void> _openInBrowser() async {
@@ -137,7 +185,10 @@ class _CustomWebViewScreenState extends State<CustomWebViewScreen> {
137185
),
138186
body: Stack(
139187
children: [
140-
WebViewWidget(controller: _controller),
188+
if (_controller != null)
189+
WebViewWidget(controller: _controller!)
190+
else
191+
const SizedBox.shrink(),
141192
if (_isLoading)
142193
Center(
143194
child: CircularProgressIndicator(

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: 5.1.0+247
4+
version: 5.1.2+249
55

66
environment:
77
sdk: ^3.8.0
@@ -105,7 +105,7 @@ msix_config:
105105
display_name: Defyx VPN
106106
publisher_display_name: UnboundTech UG
107107
identity_name: UnboundTechUG.6065AEC5A207
108-
msix_version: 5.0.7.247
108+
msix_version: 5.1.2.0
109109
publisher: "CN=62937938-2AE2-4C12-9ED8-4C418C0CADF2"
110110
logo_path: assets/images/logo.png
111111
capabilities: runFullTrust,internetClientServer,privateNetworkClientServer,unvirtualizedResources

0 commit comments

Comments
 (0)