overall ui bugfixes and hivemind improvements
This commit is contained in:
1 parent
a80a23e008
commit
8f32429e82
10 files changed
+125
-158
No files matched your search
@@ -2,13 +2,13 @@ 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/components/online_dot.dart';
|
import 'package:paladinvpn/components/online_dot.dart';
|
||||||
|
import 'package:paladinvpn/components/watch_ad_button.dart';
|
||||||
|
|
||||||
/// BottomBar is the bottom utility panel that surfaces server connectivity status
|
/// BottomBar is the bottom utility panel that surfaces server connectivity status
|
||||||
/// and offers the user an option to support the server when the VPN tunnel is active.
|
/// and offers the user an option to support the server when the VPN tunnel is active.
|
||||||
class BottomBar extends StatelessWidget {
|
class BottomBar extends StatelessWidget {
|
||||||
const BottomBar({super.key});
|
const BottomBar({super.key});
|
||||||
|
|
||||||
// Dark card background color for high contrast overlay
|
|
||||||
static const Color _cardBg = Color(0xFF1E2533);
|
static const Color _cardBg = Color(0xFF1E2533);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,8 +29,11 @@ class BottomBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Displays the server online/offline chip
|
// Server online/offline chip
|
||||||
OnlineDot(isOnline: isOnline),
|
OnlineDot(isOnline: isOnline),
|
||||||
|
const Spacer(),
|
||||||
|
// Support button — only visible while connected
|
||||||
|
if (isOnline) const WatchAdButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,110 +3,98 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:paladinvpn/logic/session_timer.dart';
|
import 'package:paladinvpn/logic/session_timer.dart';
|
||||||
|
|
||||||
/// ClockDisplay shows a large circular progress ring and the session countdown timer.
|
/// ClockDisplay renders inside the ConnectButton circle when the VPN is active.
|
||||||
///
|
///
|
||||||
/// The timer text glows electric cyan when a session is active and dims to slate gray when idle.
|
/// Shows a circular quota-progress arc, a glowing countdown timer, and the
|
||||||
|
/// current transfer speed — all sized to fit within the 260px button.
|
||||||
class ClockDisplay extends StatelessWidget {
|
class ClockDisplay extends StatelessWidget {
|
||||||
const ClockDisplay({super.key});
|
const ClockDisplay({super.key});
|
||||||
|
|
||||||
static const Color _cyanGlow = Color(0xFF00E5FF);
|
static const Color _cyanGlow = Color(0xFF00E5FF);
|
||||||
static const Color _dimSlate = Color(0xFF4A5568);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<SessionTimer>(
|
return Consumer<SessionTimer>(
|
||||||
builder: (context, timer, _) {
|
builder: (context, timer, _) {
|
||||||
final isLive = timer.isRunning;
|
return SizedBox(
|
||||||
final color = isLive ? _cyanGlow : _dimSlate;
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// Background quota track ring
|
||||||
|
const CustomPaint(
|
||||||
|
size: Size(200, 200),
|
||||||
|
painter: _ArcPainter(
|
||||||
|
progress: 1.0,
|
||||||
|
color: Color.fromRGBO(74, 85, 104, 0.2),
|
||||||
|
strokeWidth: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
return Column(
|
// Live quota progress ring
|
||||||
mainAxisSize: MainAxisSize.min,
|
TweenAnimationBuilder<double>(
|
||||||
children: [
|
tween: Tween(begin: 0, end: timer.progress),
|
||||||
SizedBox(
|
duration: const Duration(milliseconds: 400),
|
||||||
width: 220,
|
builder: (context, value, _) => CustomPaint(
|
||||||
height: 220,
|
size: const Size(200, 200),
|
||||||
child: Stack(
|
painter: _ArcPainter(
|
||||||
alignment: Alignment.center,
|
progress: value,
|
||||||
|
color: _cyanGlow,
|
||||||
|
strokeWidth: 4,
|
||||||
|
glow: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Center content: countdown + speed
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Background track ring representing the maximum capacity of the countdown
|
// Glowing countdown timer
|
||||||
const CustomPaint(
|
Text(
|
||||||
size: Size(220, 220),
|
timer.formatted,
|
||||||
painter: _ArcPainter(
|
style: const TextStyle(
|
||||||
progress: 1.0,
|
fontSize: 40,
|
||||||
color: Color.fromRGBO(74, 85, 104, 0.2),
|
fontWeight: FontWeight.w800,
|
||||||
strokeWidth: 4,
|
color: Colors.white,
|
||||||
|
letterSpacing: 2,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: Color.fromRGBO(0, 229, 255, 0.5),
|
||||||
|
blurRadius: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
// Live progress ring showing current remaining time relative to the session limit
|
|
||||||
if (isLive)
|
// Speed indicator
|
||||||
TweenAnimationBuilder<double>(
|
Row(
|
||||||
tween: Tween(begin: 0, end: timer.progress),
|
mainAxisSize: MainAxisSize.min,
|
||||||
duration: const Duration(milliseconds: 400),
|
children: [
|
||||||
builder: (context, value, _) => CustomPaint(
|
const Icon(Icons.speed, color: Colors.white54, size: 14),
|
||||||
size: const Size(220, 220),
|
const SizedBox(width: 4),
|
||||||
painter: _ArcPainter(
|
Text(
|
||||||
progress: value,
|
'${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s',
|
||||||
color: _cyanGlow,
|
style: const TextStyle(
|
||||||
strokeWidth: 4,
|
fontSize: 12,
|
||||||
glow: true,
|
color: Colors.white54,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
|
||||||
// Monospaced digital clock display for session countdown
|
|
||||||
AnimatedDefaultTextStyle(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'RobotoMono',
|
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
letterSpacing: 4,
|
|
||||||
color: color,
|
|
||||||
shadows: isLive
|
|
||||||
? const [
|
|
||||||
Shadow(
|
|
||||||
color: Color.fromRGBO(0, 229, 255, 0.6),
|
|
||||||
blurRadius: 20,
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
color: Color.fromRGBO(0, 229, 255, 0.3),
|
|
||||||
blurRadius: 40,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: const [],
|
|
||||||
),
|
|
||||||
child: Text(timer.formatted),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
|
|
||||||
// Subtitle state label below the timer
|
|
||||||
AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: Text(
|
|
||||||
isLive ? 'SESSION ACTIVE' : 'STANDBY',
|
|
||||||
key: ValueKey(isLive),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 3,
|
|
||||||
color: isLive
|
|
||||||
? const Color.fromRGBO(0, 229, 255, 0.7)
|
|
||||||
: const Color.fromRGBO(74, 85, 104, 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A custom painter to render a circular progress arc or background track ring.
|
|
||||||
class _ArcPainter extends CustomPainter {
|
class _ArcPainter extends CustomPainter {
|
||||||
final double progress;
|
final double progress;
|
||||||
final Color color;
|
final Color color;
|
||||||
@@ -125,7 +113,6 @@ class _ArcPainter extends CustomPainter {
|
|||||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height)
|
final rect = Rect.fromLTWH(0, 0, size.width, size.height)
|
||||||
.deflate(strokeWidth / 2);
|
.deflate(strokeWidth / 2);
|
||||||
|
|
||||||
// Optional blurred background paint to create an electric neon glow effect
|
|
||||||
if (glow) {
|
if (glow) {
|
||||||
final glowPaint = Paint()
|
final glowPaint = Paint()
|
||||||
..color = const Color.fromRGBO(0, 229, 255, 0.15)
|
..color = const Color.fromRGBO(0, 229, 255, 0.15)
|
||||||
@@ -136,7 +123,6 @@ class _ArcPainter extends CustomPainter {
|
|||||||
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint);
|
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core progress stroke paint
|
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = color
|
..color = color
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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/session_timer.dart';
|
import 'package:paladinvpn/logic/session_timer.dart';
|
||||||
|
import 'package:paladinvpn/components/clock_display.dart';
|
||||||
|
|
||||||
class ConnectButton extends StatefulWidget {
|
class ConnectButton extends StatefulWidget {
|
||||||
const ConnectButton({super.key});
|
const ConnectButton({super.key});
|
||||||
@@ -30,26 +31,38 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
_pulseAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
|
_pulseAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Listen to VpnConnection changes to drive the pulse animation
|
||||||
|
// from outside build(), keeping build() pure.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<VpnConnection>().addListener(_onVpnStateChanged);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
context.read<VpnConnection>().removeListener(_onVpnStateChanged);
|
||||||
_pulseController.dispose();
|
_pulseController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onVpnStateChanged() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final isConnected = context.read<VpnConnection>().status == VpnStatus.connected;
|
||||||
|
if (isConnected && !_pulseController.isAnimating) {
|
||||||
|
_pulseController.repeat(reverse: true);
|
||||||
|
} else if (!isConnected && _pulseController.isAnimating) {
|
||||||
|
_pulseController.stop();
|
||||||
|
_pulseController.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTap() async {
|
void _handleTap() async {
|
||||||
if (_inCooldown) return;
|
if (_inCooldown) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() => _inCooldown = true);
|
||||||
_inCooldown = true;
|
|
||||||
});
|
|
||||||
Future.delayed(const Duration(seconds: 5), () {
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
if (mounted) {
|
if (mounted) setState(() => _inCooldown = false);
|
||||||
setState(() {
|
|
||||||
_inCooldown = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
@@ -58,14 +71,11 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
if (vpn.status == VpnStatus.connected) {
|
if (vpn.status == VpnStatus.connected) {
|
||||||
vpn.disconnect();
|
vpn.disconnect();
|
||||||
timer.stop();
|
timer.stop();
|
||||||
_pulseController.stop();
|
|
||||||
_pulseController.reset();
|
|
||||||
} else if (vpn.status == VpnStatus.disconnected ||
|
} else if (vpn.status == VpnStatus.disconnected ||
|
||||||
vpn.status == VpnStatus.error) {
|
vpn.status == VpnStatus.error) {
|
||||||
final success = await vpn.connect();
|
final success = await vpn.connect();
|
||||||
if (success) {
|
if (success) {
|
||||||
timer.start('main_ad');
|
timer.start('main_ad');
|
||||||
_pulseController.repeat(reverse: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,13 +87,6 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
final isConnected = vpn.status == VpnStatus.connected;
|
final isConnected = vpn.status == VpnStatus.connected;
|
||||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||||
|
|
||||||
if (isConnected && !_pulseController.isAnimating) {
|
|
||||||
_pulseController.repeat(reverse: true);
|
|
||||||
} else if (!isConnected && !isConnecting && _pulseController.isAnimating) {
|
|
||||||
_pulseController.stop();
|
|
||||||
_pulseController.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: (isConnecting || _inCooldown) ? null : _handleTap,
|
onTap: (isConnecting || _inCooldown) ? null : _handleTap,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
@@ -121,8 +124,8 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
: const [],
|
: const [],
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: isConnected
|
child: isConnected
|
||||||
? _buildConnectedStats(timer)
|
? const ClockDisplay()
|
||||||
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -164,44 +167,4 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildConnectedStats(SessionTimer timer) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Countdown Timer
|
|
||||||
Text(
|
|
||||||
timer.formatted,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
color: Colors.white,
|
|
||||||
letterSpacing: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Speed indicator
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.speed, color: Colors.white70, size: 16),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white70,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class InfoButton extends StatelessWidget {
|
class InfoButton extends StatelessWidget {
|
||||||
const InfoButton({Key? key}) : super(key: key);
|
const InfoButton({super.key});
|
||||||
|
|
||||||
Future<void> _launchGitHub() async {
|
Future<void> _launchGitHub() async {
|
||||||
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
|
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
|
||||||
|
|||||||
@@ -52,14 +52,21 @@ class StatusText extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
// Dynamic status icon (spinning arrows for loading, outlined shield for idle)
|
if (isConnecting)
|
||||||
Icon(
|
const SizedBox(
|
||||||
isConnecting ? Icons.autorenew : Icons.shield_outlined,
|
width: 14,
|
||||||
size: 18,
|
height: 14,
|
||||||
color: isConnecting
|
child: CircularProgressIndicator(
|
||||||
? const Color(0xFF00E5FF)
|
strokeWidth: 1.5,
|
||||||
: const Color(0xAA4A5568),
|
color: Color(0xFF00E5FF),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xAA4A5568),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
vpn.statusMessage.toUpperCase(),
|
vpn.statusMessage.toUpperCase(),
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class AdManager extends ChangeNotifier {
|
|||||||
final keys = await CryptoService.getOrCreateKeys();
|
final keys = await CryptoService.getOrCreateKeys();
|
||||||
final pubKey = keys['publicKey']!;
|
final pubKey = keys['publicKey']!;
|
||||||
|
|
||||||
// 3. Attach custom data to the ad so Google sends it back to our Hivemind server
|
// 3. Build SSV options to send to our Hivemind server via Google's callback
|
||||||
_rewardedAd!.serverSideVerificationOptions = ServerSideVerificationOptions(
|
final ssvOptions = ServerSideVerificationOptions(
|
||||||
customData: jsonEncode({
|
customData: jsonEncode({
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
'public_key': pubKey,
|
'public_key': pubKey,
|
||||||
@@ -130,7 +130,8 @@ class AdManager extends ChangeNotifier {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Show the ad
|
// 5. Show the ad with SSV options
|
||||||
|
_rewardedAd!.setServerSideOptions(ssvOptions);
|
||||||
await _rewardedAd!.show(
|
await _rewardedAd!.show(
|
||||||
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
|
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
|
||||||
debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}');
|
debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}');
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class SessionTimer extends ChangeNotifier {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
VpnNotificationManager.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 2: Start the WireGuard tunnel with the fetched config ─────────
|
// ── Step 3: Start the WireGuard tunnel with the fetched config ─────────
|
||||||
try {
|
try {
|
||||||
// Re-initialize to ensure VPN permission is granted before starting
|
// Re-initialize to ensure VPN permission is granted before starting
|
||||||
await _wireguard.initialize(interfaceName: 'Paladin0');
|
await _wireguard.initialize(interfaceName: 'Paladin0');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/screens/main_screen.dart';
|
import 'package:paladinvpn/screens/main_screen.dart';
|
||||||
import 'package:paladinvpn/screens/intro.dart';
|
import 'package:paladinvpn/screens/intro.dart';
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ class PaladinApp extends StatelessWidget {
|
|||||||
update: (_, vpn, prev) =>
|
update: (_, vpn, prev) =>
|
||||||
prev ?? SessionTimer(vpnConnection: vpn),
|
prev ?? SessionTimer(vpnConnection: vpn),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Manages rewarded ad lifecycle — preloads on startup.
|
||||||
|
ChangeNotifierProvider(create: (_) => AdManager()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Paladin VPN',
|
title: 'Paladin VPN',
|
||||||
|
|||||||
@@ -447,7 +447,9 @@ def admob_callback():
|
|||||||
if not device_id or not client_pubkey:
|
if not device_id or not client_pubkey:
|
||||||
return "Invalid custom_data payload", 400
|
return "Invalid custom_data payload", 400
|
||||||
|
|
||||||
success, msg = _start_or_extend_session(device_id, client_pubkey, 'main_ad')
|
ad_type = custom_data.get('ad_type', 'main_ad')
|
||||||
|
|
||||||
|
success, msg = _start_or_extend_session(device_id, client_pubkey, ad_type)
|
||||||
return ("OK", 200) if success else (msg, 500)
|
return ("OK", 200) if success else (msg, 500)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in new issue
Block a user