User-Agent spoofed to Chrome on Android (was Dart/3.x). Domain renamed to userevolt.app. Branding to Revolt VPN. Removed duplicate notification, patched flutter_vless for custom icon + dark background. Merged 5 disconnect paths into single idempotent gate, fixed stopVless timeout silently lying about state. Removed kDebugMode guard from AdMob bypass.
170 lines
5.0 KiB
Dart
170 lines
5.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
|
import 'package:revoltvpn/logic/crypto_service.dart';
|
|
import 'package:revoltvpn/logic/hivemind_service.dart';
|
|
import 'package:revoltvpn/logic/app_config.dart';
|
|
|
|
class AdManager extends ChangeNotifier {
|
|
static const bool adsEnabled = false;
|
|
|
|
RewardedAd? _rewardedAd;
|
|
|
|
bool _isAdLoaded = false;
|
|
bool get isAdLoaded => adsEnabled ? _isAdLoaded : true;
|
|
|
|
bool _isAdLoading = false;
|
|
bool get isAdLoading => adsEnabled ? _isAdLoading : false;
|
|
|
|
Completer<bool>? _loadCompleter;
|
|
|
|
// Google-provided test ad unit
|
|
static String get _adUnitId => AppConfig.adUnitId;
|
|
|
|
AdManager() {
|
|
if (adsEnabled) preloadAd();
|
|
}
|
|
|
|
Future<bool> preloadAd() async {
|
|
if (!adsEnabled) return true;
|
|
if (_isAdLoaded) return true;
|
|
if (_isAdLoading) return _loadCompleter?.future ?? Future.value(false);
|
|
|
|
_isAdLoading = true;
|
|
_loadCompleter = Completer<bool>();
|
|
notifyListeners();
|
|
|
|
if (kIsWeb) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
_isAdLoaded = true;
|
|
_isAdLoading = false;
|
|
notifyListeners();
|
|
_loadCompleter?.complete(true);
|
|
return true;
|
|
}
|
|
|
|
RewardedAd.load(
|
|
adUnitId: _adUnitId,
|
|
request: const AdRequest(),
|
|
rewardedAdLoadCallback: RewardedAdLoadCallback(
|
|
onAdLoaded: (ad) {
|
|
_rewardedAd = ad;
|
|
_isAdLoaded = true;
|
|
_isAdLoading = false;
|
|
notifyListeners();
|
|
if (!(_loadCompleter?.isCompleted ?? true)) {
|
|
_loadCompleter?.complete(true);
|
|
}
|
|
},
|
|
onAdFailedToLoad: (error) {
|
|
debugPrint('Rewarded ad failed to load: ${error.message}');
|
|
_isAdLoaded = false;
|
|
_isAdLoading = false;
|
|
notifyListeners();
|
|
if (!(_loadCompleter?.isCompleted ?? true)) {
|
|
_loadCompleter?.complete(false);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
|
|
return _loadCompleter!.future;
|
|
}
|
|
|
|
// ── Show ad (or debug bypass) ─────────────────────────────────────
|
|
|
|
Future<bool> showAd(String adType) async {
|
|
// Debug bypass: fire fake AdMob callback so support ads work in dev.
|
|
// The server's ADMOB_BYPASS must be True for this to succeed.
|
|
if (!adsEnabled && kDebugMode) {
|
|
final deviceId = await CryptoService.getDeviceId();
|
|
final nonce = '${Random().nextInt(0x7FFFFFFF)}-${DateTime.now().millisecondsSinceEpoch}';
|
|
HivemindService.setExpectedNonce(nonce);
|
|
try {
|
|
final customData = jsonEncode({
|
|
'device_id': deviceId,
|
|
'ad_type': adType,
|
|
'nonce': nonce,
|
|
});
|
|
final fakeUrl = Uri.parse(
|
|
'${AppConfig.hivemindApiPublic}/admob/callback'
|
|
'?signature=test&key_id=test'
|
|
'&custom_data=${Uri.encodeComponent(customData)}');
|
|
await HivemindService.directGet(fakeUrl, timeout: const Duration(seconds: 8));
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!adsEnabled) return false;
|
|
|
|
if (!_isAdLoaded || _rewardedAd == null) {
|
|
final loaded = await preloadAd();
|
|
if (!loaded || _rewardedAd == null) {
|
|
debugPrint('[AdManager] Cannot show ad, failed to load.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
final deviceId = await CryptoService.getDeviceId();
|
|
|
|
final nonce = '${Random().nextInt(0x7FFFFFFF)}-${DateTime.now().millisecondsSinceEpoch}';
|
|
HivemindService.setExpectedNonce(nonce);
|
|
debugPrint('[AdManager] Ad nonce: $nonce');
|
|
|
|
final ssvOptions = ServerSideVerificationOptions(
|
|
customData: jsonEncode({
|
|
'device_id': deviceId,
|
|
'ad_type': adType,
|
|
'nonce': nonce,
|
|
}),
|
|
);
|
|
|
|
Completer<bool> rewardCompleter = Completer<bool>();
|
|
|
|
_rewardedAd!.fullScreenContentCallback = FullScreenContentCallback(
|
|
onAdShowedFullScreenContent: (ad) => debugPrint('[AdManager] Ad showing.'),
|
|
onAdDismissedFullScreenContent: (ad) {
|
|
debugPrint('[AdManager] Ad dismissed.');
|
|
ad.dispose();
|
|
_isAdLoaded = false;
|
|
_rewardedAd = null;
|
|
preloadAd();
|
|
if (!rewardCompleter.isCompleted) {
|
|
rewardCompleter.complete(false);
|
|
}
|
|
},
|
|
onAdFailedToShowFullScreenContent: (ad, error) {
|
|
debugPrint('[AdManager] Ad failed to show: $error');
|
|
ad.dispose();
|
|
_isAdLoaded = false;
|
|
_rewardedAd = null;
|
|
if (!rewardCompleter.isCompleted) {
|
|
rewardCompleter.complete(false);
|
|
}
|
|
},
|
|
);
|
|
|
|
_rewardedAd!.setServerSideOptions(ssvOptions);
|
|
await _rewardedAd!.show(
|
|
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
|
|
debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}');
|
|
if (!rewardCompleter.isCompleted) {
|
|
rewardCompleter.complete(true);
|
|
}
|
|
},
|
|
);
|
|
|
|
return rewardCompleter.future;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_rewardedAd?.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|