@@ -82,6 +82,24 @@ class InternalAdStrategy implements AdLoadingStrategy {
8282 );
8383 }
8484
85+ // Validate URL format before using it (safety check for iOS network issue)
86+ if (! imageUrl.startsWith ('http://' ) && ! imageUrl.startsWith ('https://' )) {
87+ debugPrint ('❌ Invalid internal ad URL format: $imageUrl ' );
88+ await _analytics.logEvent (
89+ name: 'ads_internal_ad_load_failure' ,
90+ parameters: {'error_code' : 'INVALID_URL' , 'url' : imageUrl},
91+ );
92+
93+ ref.read (adsProvider.notifier).setAdLoadFailed (
94+ errorCode: 'INVALID_URL' ,
95+ errorMessage: 'Invalid ad URL format' ,
96+ );
97+ return AdLoadResult .failure (
98+ errorCode: 'INVALID_URL' ,
99+ errorMessage: 'Invalid ad URL format: $imageUrl ' ,
100+ );
101+ }
102+
85103 debugPrint ('✅ Internal ad loaded:' );
86104 debugPrint (' Image: $imageUrl ' );
87105 debugPrint (' Click: $clickUrl ' );
@@ -125,6 +143,16 @@ class InternalAdStrategy implements AdLoadingStrategy {
125143 final imageUrl = state.customImageUrl ?? '' ;
126144 final clickUrl = state.customClickUrl ?? '' ;
127145
146+ // Validate URL format before rendering (defensive check to prevent iOS file:/// error)
147+ // CRITICAL: Never pass empty or malformed URLs to Image.network on iOS
148+ // iOS incorrectly resolves empty strings as file:/// URIs causing crashes
149+ if (imageUrl.isEmpty || (! imageUrl.startsWith ('http://' ) && ! imageUrl.startsWith ('https://' ))) {
150+ if (imageUrl.isNotEmpty) {
151+ debugPrint ('❌ Invalid URL in buildAdWidget, refusing to render: $imageUrl ' );
152+ }
153+ return const SizedBox .shrink ();
154+ }
155+
128156 return GestureDetector (
129157 onTap: () async {
130158 if (clickUrl.isEmpty) {
@@ -200,8 +228,9 @@ class InternalAdStrategy implements AdLoadingStrategy {
200228 },
201229 );
202230
203- // Trigger state update to hide the entire widget
204- container.read (adsProvider.notifier).setCustomImageLoadFailed ();
231+ // Clear all ad data so the container disappears completely
232+ debugPrint ('🗑️ Clearing ad data due to image load failure' );
233+ container.read (adsProvider.notifier).clearCustomAdData ();
205234 });
206235 }
207236
@@ -225,17 +254,25 @@ class InternalAdStrategy implements AdLoadingStrategy {
225254 // INTERNAL ADS: Show when connected (all users including Iranian)
226255 // When connected, load fresh internal ad and show it
227256 if (current == ConnectionStatus .connected && previous != ConnectionStatus .connected) {
228- debugPrint ('▶️ Connected - loading fresh internal ad to show during VPN session ' );
257+ debugPrint ('▶️ Connected - will load internal ad after network routing stabilizes ' );
229258
230259 // Mark first connection complete
231260 ref.read (adsProvider.notifier).markFirstConnectionComplete ();
232261
233- // Load fresh internal ad
234- loadAd (ref: ref).then ((result) {
235- if (result.success) {
236- debugPrint ('⏰ Fresh internal ad loaded - starting countdown' );
237- ref.read (adsProvider.notifier).startCountdownTimer ();
238- }
262+ // iOS FIX: Add delay to allow VPN network routing to fully establish
263+ // Without this delay, Image.network may incorrectly resolve HTTPS URLs as file:// URIs
264+ // causing "No host specified in URI file:///..." errors on first connection
265+ // Also allows time for VPN tunnel to stabilize (tun2socks, ping refresh, SSL/TLS)
266+ Future .delayed (const Duration (milliseconds: 2500 ), () {
267+ debugPrint ('⏱️ Network routing delay complete (2.5s) - loading fresh internal ad' );
268+
269+ // Load fresh internal ad
270+ loadAd (ref: ref).then ((result) {
271+ if (result.success) {
272+ debugPrint ('⏰ Fresh internal ad loaded - starting countdown' );
273+ ref.read (adsProvider.notifier).startCountdownTimer ();
274+ }
275+ });
239276 });
240277 return ;
241278 }
0 commit comments