general small bugfixes
This commit is contained in:
1 parent
0c39e0c5ee
commit
128a3b1320
5 files changed
+41
-34
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||||
|
import 'package:paladinvpn/logic/ad_manager.dart';
|
||||||
import 'package:paladinvpn/components/online_dot.dart';
|
import 'package:paladinvpn/components/online_dot.dart';
|
||||||
import 'package:paladinvpn/components/watch_ad_button.dart';
|
import 'package:paladinvpn/components/watch_ad_button.dart';
|
||||||
|
|
||||||
@@ -32,8 +33,8 @@ class BottomBar extends StatelessWidget {
|
|||||||
// Server online/offline chip
|
// Server online/offline chip
|
||||||
OnlineDot(isOnline: isOnline),
|
OnlineDot(isOnline: isOnline),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Support button — only visible while connected
|
// Support button — only visible while connected AND ads are enabled
|
||||||
if (isOnline) const WatchAdButton(),
|
if (isOnline && AdManager.adsEnabled) const WatchAdButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||||
import 'package:paladinvpn/logic/session_timer.dart';
|
import 'package:paladinvpn/logic/session_timer.dart';
|
||||||
import 'package:paladinvpn/logic/ad_manager.dart';
|
|
||||||
import 'package:paladinvpn/components/clock_display.dart';
|
import 'package:paladinvpn/components/clock_display.dart';
|
||||||
|
|
||||||
class ConnectButton extends StatefulWidget {
|
class ConnectButton extends StatefulWidget {
|
||||||
@@ -64,7 +63,6 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
|
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
final timer = context.read<SessionTimer>();
|
final timer = context.read<SessionTimer>();
|
||||||
final ad = context.read<AdManager>();
|
|
||||||
|
|
||||||
if (vpn.status == VpnStatus.connected) {
|
if (vpn.status == VpnStatus.connected) {
|
||||||
// Disconnect — apply a short cooldown to prevent rapid toggle spam
|
// Disconnect — apply a short cooldown to prevent rapid toggle spam
|
||||||
@@ -77,17 +75,6 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
});
|
});
|
||||||
} else if (vpn.status == VpnStatus.disconnected ||
|
} else if (vpn.status == VpnStatus.disconnected ||
|
||||||
vpn.status == VpnStatus.error) {
|
vpn.status == VpnStatus.error) {
|
||||||
// Only attempt connect if an ad is loaded (or we're in dev/web mode)
|
|
||||||
if (!ad.isAdLoaded && !ad.isAdLoading) {
|
|
||||||
ad.preloadAd(); // trigger a reload in background
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Ad is not ready yet. Please wait a moment.'),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
final success = await vpn.connect();
|
final success = await vpn.connect();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -105,8 +92,8 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer3<VpnConnection, SessionTimer, AdManager>(
|
return Consumer2<VpnConnection, SessionTimer>(
|
||||||
builder: (context, vpn, timer, ad, _) {
|
builder: (context, vpn, timer, _) {
|
||||||
final isConnected = vpn.status == VpnStatus.connected;
|
final isConnected = vpn.status == VpnStatus.connected;
|
||||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||||
|
|
||||||
@@ -149,7 +136,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: isConnected
|
child: isConnected
|
||||||
? const ClockDisplay()
|
? const ClockDisplay()
|
||||||
: _buildDisconnectedIcon(vpn.status, _inCooldown, ad),
|
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -159,7 +146,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown, AdManager ad) {
|
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown) {
|
||||||
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
|
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
@@ -183,10 +170,6 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
label = 'TRY AGAIN';
|
label = 'TRY AGAIN';
|
||||||
icon = Icons.refresh;
|
icon = Icons.refresh;
|
||||||
tint = const Color(0xFFEF5350); // soft red — something went wrong
|
tint = const Color(0xFFEF5350); // soft red — something went wrong
|
||||||
} else if (!ad.isAdLoaded && !ad.isAdLoading) {
|
|
||||||
label = 'LOADING AD…';
|
|
||||||
icon = Icons.hourglass_empty;
|
|
||||||
tint = const Color.fromRGBO(74, 85, 104, 0.7);
|
|
||||||
} else {
|
} else {
|
||||||
label = 'TAP TO CONNECT';
|
label = 'TAP TO CONNECT';
|
||||||
icon = Icons.power_settings_new;
|
icon = Icons.power_settings_new;
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ import 'package:paladinvpn/logic/app_config.dart';
|
|||||||
|
|
||||||
/// AdManager manages the lifecycle of rewarded video ads.
|
/// AdManager manages the lifecycle of rewarded video ads.
|
||||||
///
|
///
|
||||||
/// It acts as a strict gatekeeper: users MUST watch an ad to connect.
|
/// When [adsEnabled] is false (pre-launch mode) all ad operations are
|
||||||
/// If an ad is not ready when they click connect, it forces them to wait
|
/// silently skipped — the app connects without any ad requirement.
|
||||||
/// while it loads. If they dismiss the ad early, they do not get the connection.
|
|
||||||
class AdManager extends ChangeNotifier {
|
class AdManager extends ChangeNotifier {
|
||||||
|
/// Flip to `true` after Google Play launch when real ads are wired up.
|
||||||
|
static const bool adsEnabled = false;
|
||||||
|
|
||||||
RewardedAd? _rewardedAd;
|
RewardedAd? _rewardedAd;
|
||||||
|
|
||||||
bool _isAdLoaded = false;
|
bool _isAdLoaded = false;
|
||||||
bool get isAdLoaded => _isAdLoaded;
|
bool get isAdLoaded => adsEnabled ? _isAdLoaded : true;
|
||||||
|
|
||||||
bool _isAdLoading = false;
|
bool _isAdLoading = false;
|
||||||
bool get isAdLoading => _isAdLoading;
|
bool get isAdLoading => adsEnabled ? _isAdLoading : false;
|
||||||
|
|
||||||
Completer<bool>? _loadCompleter;
|
Completer<bool>? _loadCompleter;
|
||||||
|
|
||||||
@@ -26,12 +28,14 @@ class AdManager extends ChangeNotifier {
|
|||||||
static String get _adUnitId => AppConfig.adUnitId;
|
static String get _adUnitId => AppConfig.adUnitId;
|
||||||
|
|
||||||
AdManager() {
|
AdManager() {
|
||||||
_preloadAd();
|
if (adsEnabled) preloadAd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a rewarded ad in the background and returns a Future when ready.
|
/// Loads a rewarded ad in the background and returns a Future when ready.
|
||||||
/// Safe to call multiple times — re-entrant calls wait on the same load.
|
/// Safe to call multiple times — re-entrant calls wait on the same load.
|
||||||
Future<bool> _preloadAd() async {
|
/// No-op when [adsEnabled] is false.
|
||||||
|
Future<bool> preloadAd() async {
|
||||||
|
if (!adsEnabled) return true;
|
||||||
// If already loaded, return instantly
|
// If already loaded, return instantly
|
||||||
if (_isAdLoaded) return true;
|
if (_isAdLoaded) return true;
|
||||||
|
|
||||||
@@ -80,10 +84,13 @@ class AdManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Displays the rewarded video ad to the user.
|
/// Displays the rewarded video ad to the user.
|
||||||
|
/// Returns false immediately when [adsEnabled] is false.
|
||||||
Future<bool> showAd(String adType) async {
|
Future<bool> showAd(String adType) async {
|
||||||
|
if (!adsEnabled) return false;
|
||||||
|
|
||||||
// 1. Ensure the ad is fully loaded before trying to show it
|
// 1. Ensure the ad is fully loaded before trying to show it
|
||||||
if (!_isAdLoaded || _rewardedAd == null) {
|
if (!_isAdLoaded || _rewardedAd == null) {
|
||||||
final loaded = await _preloadAd();
|
final loaded = await preloadAd();
|
||||||
if (!loaded || _rewardedAd == null) {
|
if (!loaded || _rewardedAd == null) {
|
||||||
debugPrint('[AdManager] Cannot show ad, failed to load.');
|
debugPrint('[AdManager] Cannot show ad, failed to load.');
|
||||||
return false;
|
return false;
|
||||||
@@ -115,7 +122,7 @@ class AdManager extends ChangeNotifier {
|
|||||||
_isAdLoaded = false;
|
_isAdLoaded = false;
|
||||||
_rewardedAd = null;
|
_rewardedAd = null;
|
||||||
// Start loading the next ad immediately
|
// Start loading the next ad immediately
|
||||||
_preloadAd();
|
preloadAd();
|
||||||
if (!rewardCompleter.isCompleted) {
|
if (!rewardCompleter.isCompleted) {
|
||||||
rewardCompleter.complete(false); // User closed before earning reward
|
rewardCompleter.complete(false); // User closed before earning reward
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,21 @@ class HivemindService {
|
|||||||
|
|
||||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||||
|
|
||||||
// AdMob SSV is handled server-to-server by Google.
|
// ── TEMPORARY AD BYPASS HACK FOR TESTING ──
|
||||||
// The app waits for the server to activate the session via the real callback.
|
// Since the crypto signature check is disabled on the Python server,
|
||||||
|
// we send a fake "Ad Watched" ping from the app directly.
|
||||||
|
// TODO: Remove this when real AdMob SSV is enabled on the server.
|
||||||
|
try {
|
||||||
|
final customData = jsonEncode({
|
||||||
|
'device_id': deviceId,
|
||||||
|
'public_key': keys['publicKey']!,
|
||||||
|
'ad_type': 'main_ad',
|
||||||
|
});
|
||||||
|
final fakeAdmobPingUrl = Uri.parse(
|
||||||
|
'${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
|
||||||
|
await http.get(fakeAdmobPingUrl).timeout(const Duration(seconds: 8));
|
||||||
|
} catch (_) {}
|
||||||
|
// ──────────────────────────────────────────
|
||||||
|
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
while (attempts < 15) {
|
while (attempts < 15) {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
VpnStatus _status = VpnStatus.disconnected;
|
VpnStatus _status = VpnStatus.disconnected;
|
||||||
VpnStatus get status => _status;
|
VpnStatus get status => _status;
|
||||||
|
|
||||||
|
bool _pluginAvailable = false;
|
||||||
|
|
||||||
// True only during the brief window after startup where we detected an
|
// True only during the brief window after startup where we detected an
|
||||||
// already-running tunnel — lets SessionTimer know to auto-resume.
|
// already-running tunnel — lets SessionTimer know to auto-resume.
|
||||||
bool _isStartupRestoration = false;
|
bool _isStartupRestoration = false;
|
||||||
@@ -80,6 +82,7 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
// Register our tunnel interface with Android.
|
// Register our tunnel interface with Android.
|
||||||
// 'Paladin0' is just the name of the network interface — like "eth0"
|
// 'Paladin0' is just the name of the network interface — like "eth0"
|
||||||
await _wireguard.initialize(interfaceName: 'Paladin0');
|
await _wireguard.initialize(interfaceName: 'Paladin0');
|
||||||
|
_pluginAvailable = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('WireGuard init error (expected on emulator): $e');
|
debugPrint('WireGuard init error (expected on emulator): $e');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user