Created
September 9, 2025 05:19
-
-
Save imnnquy/44867cd43c668e16f60a486800b4ca2c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:async'; | |
| import 'package:connectivity_plus/connectivity_plus.dart'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:google_mobile_ads/google_mobile_ads.dart'; | |
| import 'package:loader_overlay/loader_overlay.dart'; | |
| import 'package:provider/provider.dart'; | |
| import 'package:tapcraft_hair_clipper/providers/subscription_provider.dart'; | |
| import 'package:tapcraft_hair_clipper/services/logger_service/logger_constants.dart'; | |
| import 'package:tapcraft_hair_clipper/services/logger_service/logger_service.dart'; | |
| import 'package:tapcraft_hair_clipper/utils/navigation_service.dart'; | |
| class PreloadAdManager { | |
| static final PreloadAdManager _instance = PreloadAdManager._internal(); | |
| factory PreloadAdManager() => _instance; | |
| PreloadAdManager._internal(); | |
| InterstitialAd? _preloadedInterstitialAd; | |
| bool _isInterstitialLoading = false; | |
| DateTime? _interstitialLoadTime; | |
| static const Duration _maxCacheTime = Duration(minutes: 30); | |
| /// SỬA LỖI: Khởi tạo là 'true' để logic bỏ qua lần đầu hoạt động. | |
| bool firstLaunch = true; | |
| /// Preload Interstitial Ad | |
| Future<bool> preloadInterstitialAd({ | |
| required String adUnitId, | |
| Function? onPreloadSuccess, | |
| Function? onPreloadFailed, | |
| }) async { | |
| // Logic kiểm tra này BÂY GIỜ ĐÃ ĐÚNG, vì quảng cáo sẽ bị gán null ngay khi BẮT ĐẦU hiển thị, | |
| // cho phép việc tải trước quảng cáo 'tiếp theo' được gọi ngay sau đó. | |
| if (_isInterstitialLoading || _isAdStillValid(_interstitialLoadTime)) { | |
| return _preloadedInterstitialAd != null; | |
| } | |
| if (!await _canLoadAds()) { | |
| onPreloadFailed?.call(); | |
| return false; | |
| } | |
| _isInterstitialLoading = true; | |
| debugPrint('🔄 Preloading Interstitial Ad: $adUnitId'); | |
| try { | |
| await InterstitialAd.load( | |
| adUnitId: adUnitId, | |
| request: const AdRequest(), | |
| adLoadCallback: InterstitialAdLoadCallback( | |
| onAdLoaded: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adLoaded, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'preloaded_interstitial', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| _preloadedInterstitialAd = ad; | |
| _interstitialLoadTime = DateTime.now(); | |
| _isInterstitialLoading = false; | |
| debugPrint('✅ Interstitial Ad preloaded successfully'); | |
| onPreloadSuccess?.call(); | |
| }, | |
| onAdFailedToLoad: (error) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adFailedToLoad, | |
| parameters: { | |
| 'code': error.code, | |
| 'domain': error.domain, | |
| 'message': error.message, | |
| 'type': 'preloaded_interstitial', | |
| 'responseInfo': error.responseInfo.toString(), | |
| }, | |
| ); | |
| _isInterstitialLoading = false; | |
| debugPrint('❌ Failed to preload Interstitial Ad: ${error.message}'); | |
| onPreloadFailed?.call(); | |
| }, | |
| ), | |
| ); | |
| } catch (e) { | |
| _isInterstitialLoading = false; | |
| debugPrint('❌ Exception while preloading Interstitial Ad: $e'); | |
| onPreloadFailed?.call(); | |
| } | |
| return _preloadedInterstitialAd != null; | |
| } | |
| /// Show preloaded Interstitial Ad | |
| /// HÀM NÀY CHỈ HIỂN THỊ, KHÔNG TẢI TRƯỚC QUẢNG CÁO TIẾP THEO. | |
| Future<bool> showPreloadedInterstitialAd({ | |
| Function? onAdShown, | |
| Function? onAdDismissed, | |
| Function? onAdFailed, | |
| bool showLoadingOverlay = true, | |
| }) async { | |
| final context = NavigationService.rootNavigatorKey.currentContext; | |
| if (context == null) { | |
| debugPrint('❌ No context available for showing ad'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| final isPremium = context.read<SubscriptionProvider>().isSubscribed; | |
| if (isPremium) { | |
| debugPrint('ℹ️ Premium user, skipping ad'); | |
| onAdDismissed?.call(); | |
| return true; | |
| } | |
| if (_preloadedInterstitialAd == null || | |
| !_isAdStillValid(_interstitialLoadTime)) { | |
| debugPrint('❌ No valid preloaded interstitial ad available'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.show(); | |
| } | |
| // SỬA LỖI RACE CONDITION: | |
| // 1. Sao chép ad vào biến cục bộ. | |
| final adToShow = _preloadedInterstitialAd!; | |
| // 2. Xóa trạng thái chung NGAY LẬP TỨC. | |
| // Điều này "tiêu thụ" quảng cáo và cho phép lần tải trước tiếp theo (nếu được gọi) hoạt động. | |
| _preloadedInterstitialAd = null; | |
| _interstitialLoadTime = null; | |
| try { | |
| adToShow.fullScreenContentCallback = | |
| FullScreenContentCallback( | |
| onAdShowedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adShown, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'preloaded_interstitial', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| debugPrint('✅ Preloaded Interstitial Ad shown'); | |
| onAdShown?.call(); | |
| }, | |
| onAdDismissedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adDismissClose, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'preloaded_interstitial', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad. KHÔNG chạm vào trạng thái chung (_preloadedInterstitialAd) | |
| // vì trạng thái đó hiện thuộc về lần tải tiếp theo. | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint('✅ Preloaded Interstitial Ad dismissed'); | |
| onAdDismissed?.call(); | |
| }, | |
| onAdFailedToShowFullScreenContent: (ad, error) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adFailedToShow, | |
| parameters: { | |
| 'code': error.code, | |
| 'domain': error.domain, | |
| 'message': error.message, | |
| 'type': 'preloaded_interstitial', | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad. | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| '❌ Failed to show preloaded Interstitial Ad: ${error.message}'); | |
| onAdFailed?.call(); | |
| }, | |
| ); | |
| adToShow.show(); | |
| return true; | |
| } catch (e) { | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint('❌ Exception while showing preloaded Interstitial Ad: $e'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| } | |
| /// ĐÃ XÓA PHƯƠNG THỨC FALLBACK DƯ THỪA (preloadAndShowInterstitialAd) | |
| /// Phương thức này (showAndPreloadInterstitialAd) bây giờ là phương thức chính duy nhất | |
| /// và đã xử lý cả hai trường hợp (có và không có preload). | |
| bool _isAdStillValid(DateTime? loadTime) { | |
| if (loadTime == null) return false; | |
| return DateTime.now().difference(loadTime) < _maxCacheTime; | |
| } | |
| Future<bool> _canLoadAds() async { | |
| final context = NavigationService.rootNavigatorKey.currentContext; | |
| if (context == null) return false; | |
| final isPremium = context.read<SubscriptionProvider>().isSubscribed; | |
| if (isPremium) return false; | |
| final connectivityResult = await Connectivity().checkConnectivity(); | |
| return connectivityResult == ConnectivityResult.wifi || | |
| connectivityResult == ConnectivityResult.mobile; | |
| } | |
| /// Show ads and preload next ads (PHƯƠNG THỨC CHÍNH) | |
| Future<bool> showAndPreloadInterstitialAd({ | |
| required String adUnitId, | |
| Function? onAdShown, | |
| Function? onAdDismissed, | |
| Function? onAdFailed, | |
| bool showLoadingOverlay = false, | |
| bool preloadNext = true, | |
| }) async { | |
| if (firstLaunch) { | |
| firstLaunch = false; | |
| onAdDismissed?.call(); // Gọi dismiss để luồng code tiếp tục | |
| return true; // Trả về true (thành công) vì việc bỏ qua là dự định | |
| } | |
| final context = NavigationService.rootNavigatorKey.currentContext; | |
| if (context == null) { | |
| debugPrint('ANNE: ❌ No context available for showing ad'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| final isPremium = context.read<SubscriptionProvider>().isSubscribed; | |
| if (isPremium) { | |
| debugPrint('ANNE: ℹ️ Premium user, skipping ad'); | |
| onAdDismissed?.call(); | |
| return true; | |
| } | |
| // LUỒNG A (HAPPY PATH): Đã có quảng cáo tải trước | |
| if (_preloadedInterstitialAd != null && | |
| _isAdStillValid(_interstitialLoadTime)) { | |
| debugPrint('ANNE: 🚀 Showing preloaded ad and preloading next one'); | |
| // SỬA LỖI (BUG 1 & 2): | |
| // 1. Lấy quảng cáo ra để hiển thị | |
| final adToShow = _preloadedInterstitialAd!; | |
| // 2. Tiêu thụ quảng cáo (xóa trạng thái chung) NGAY LẬP TỨC. | |
| _preloadedInterstitialAd = null; | |
| _interstitialLoadTime = null; | |
| // 3. Bây giờ bắt đầu tải trước quảng cáo tiếp theo. | |
| // Vì trạng thái đã bị xóa, hàm preloadInterstitialAd sẽ vượt qua kiểm tra và chạy đúng. | |
| if (preloadNext) { | |
| _preloadNextInterstitialAd(adUnitId); | |
| } | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.show(); | |
| } | |
| try { | |
| adToShow.fullScreenContentCallback = | |
| FullScreenContentCallback( | |
| onAdShowedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adShown, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'show_and_preload_interstitial', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| debugPrint('ANNE: ✅ Interstitial Ad shown (with preload)'); | |
| onAdShown?.call(); | |
| }, | |
| onAdDismissedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adDismissClose, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'show_and_preload_interstitial', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad. KHÔNG CHẠM VÀO TRẠNG THÁI CHUNG. | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| 'ANNE: ✅ Interstitial Ad dismissed, next ad is being preloaded'); | |
| onAdDismissed?.call(); | |
| }, | |
| onAdFailedToShowFullScreenContent: (ad, error) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adFailedToShow, | |
| parameters: { | |
| 'code': error.code, | |
| 'domain': error.domain, | |
| 'message': error.message, | |
| 'type': 'show_and_preload_interstitial', | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad. | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| 'ANNE: ❌ Failed to show interstitial ad: ${error.message}'); | |
| onAdFailed?.call(); | |
| }, | |
| ); | |
| adToShow.show(); | |
| return true; | |
| } catch (e) { | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint('❌ Exception while showing interstitial ad: $e'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| } | |
| // LUỒNG B (FALLBACK PATH): Không có quảng cáo tải trước, tải và hiển thị ngay | |
| debugPrint( | |
| 'ANNE: 🔄 No preloaded ad, loading and showing immediately + preloading next'); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.show(); | |
| } | |
| try { | |
| await InterstitialAd.load( | |
| adUnitId: adUnitId, | |
| request: const AdRequest(), | |
| adLoadCallback: InterstitialAdLoadCallback( | |
| onAdLoaded: (ad) { | |
| // Quảng cáo A vừa tải xong (để hiển thị ngay) | |
| // Bắt đầu tải Quảng cáo B trong nền | |
| if (preloadNext) { | |
| _preloadNextInterstitialAd(adUnitId); | |
| } | |
| ad.fullScreenContentCallback = FullScreenContentCallback( | |
| onAdShowedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adShown, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'show_and_preload_interstitial_immediate', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| debugPrint( | |
| 'ANNE: ✅ Interstitial Ad shown (immediate load + preload)'); | |
| onAdShown?.call(); | |
| }, | |
| onAdDismissedFullScreenContent: (ad) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adDismissClose, | |
| parameters: { | |
| 'adId': ad.adUnitId, | |
| 'type': 'show_and_preload_interstitial_immediate', | |
| 'responseInfo': ad.responseInfo.toString(), | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad (Quảng cáo A). | |
| // Không chạm vào trạng thái chung (vì Quảng cáo B đang tải vào đó). | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| 'ANNE: ✅ Interstitial Ad dismissed, next ad is being preloaded'); | |
| onAdDismissed?.call(); | |
| }, | |
| onAdFailedToShowFullScreenContent: (ad, error) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adFailedToShow, | |
| parameters: { | |
| 'code': error.code, | |
| 'domain': error.domain, | |
| 'message': error.message, | |
| 'type': 'show_and_preload_interstitial_immediate', | |
| }, | |
| ); | |
| // SỬA LỖI (BUG 2): Chỉ dispose ad. | |
| ad.dispose(); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| 'ANNE: ❌ Failed to show interstitial ad: ${error.message}'); | |
| onAdFailed?.call(); | |
| }, | |
| ); | |
| ad.show(); // Hiển thị Quảng cáo A | |
| }, | |
| onAdFailedToLoad: (LoadAdError error) { | |
| LoggerService.log( | |
| eventName: LoggerConstant.adFailedToLoad, | |
| parameters: { | |
| 'code': error.code, | |
| 'domain': error.domain, | |
| 'message': error.message, | |
| 'type': 'show_and_preload_interstitial_immediate', | |
| 'responseInfo': error.responseInfo.toString(), | |
| }, | |
| ); | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint( | |
| 'ANNE: ❌ Failed to load interstitial ad: ${error.message}'); | |
| onAdFailed?.call(); | |
| }, | |
| ), | |
| ); | |
| return true; | |
| } catch (e) { | |
| if (showLoadingOverlay) { | |
| context.loaderOverlay.hide(); | |
| } | |
| debugPrint('ANNE: ❌ Exception while loading interstitial ad: $e'); | |
| onAdFailed?.call(); | |
| return false; | |
| } | |
| } | |
| void _preloadNextInterstitialAd(String adUnitId) { | |
| // Hàm này chỉ là một trình bao bọc (wrapper) để gọi hàm preload chính | |
| // Nó ngăn chặn nhiều yêu cầu tải cùng lúc nếu một yêu cầu đã chạy. | |
| if (_isInterstitialLoading) { | |
| debugPrint('⏳ Already preloading interstitial ad, skipping...'); | |
| return; | |
| } | |
| debugPrint('🔄 Preloading next interstitial ad in background...'); | |
| // Cứ gọi và không cần chờ (fire-and-forget) | |
| preloadInterstitialAd( | |
| adUnitId: adUnitId, | |
| onPreloadSuccess: () { | |
| debugPrint('✅ Next interstitial ad preloaded successfully'); | |
| }, | |
| onPreloadFailed: () { | |
| debugPrint('❌ Failed to preload next interstitial ad'); | |
| }, | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment