vpn_notice 0.1.0
vpn_notice: ^0.1.0 copied to clipboard
Detect active VPN connections and show a customizable notice — banner, snackbar, dialog, or bottom sheet — with zero third-party Dart dependencies.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:vpn_notice/vpn_notice.dart';
void main() {
runApp(const VpnNoticeExampleApp());
}
class VpnNoticeExampleApp extends StatelessWidget {
const VpnNoticeExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'vpn_notice example',
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
final _controller = VpnNoticeController();
OverlayEntry? _customNoticeEntry;
@override
void initState() {
super.initState();
_controller.start();
}
@override
void dispose() {
_customNoticeEntry?.remove();
_controller.dispose();
super.dispose();
}
static const _style = VpnNoticeStyle(
title: 'VPN detected',
message: 'Some content may load differently while a VPN is active.',
actionLabel: 'Got it',
);
void _showCustomNotice() {
_customNoticeEntry?.remove();
late final OverlayEntry entry;
entry = OverlayEntry(
builder: (context) => _CustomVpnNotice(
onDismiss: () {
if (entry.mounted) entry.remove();
if (identical(_customNoticeEntry, entry)) {
_customNoticeEntry = null;
}
},
),
);
_customNoticeEntry = entry;
Overlay.of(context).insert(entry);
}
@override
Widget build(BuildContext context) {
return VpnNoticeGate(
controller: _controller,
builder: (_, dismiss) => _CustomVpnNotice(onDismiss: dismiss),
child: Scaffold(
appBar: AppBar(title: const Text('vpn_notice example')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder<bool>(
valueListenable: _controller,
builder: (context, isVpnActive, _) => Text(
isVpnActive
? 'Live status: VPN is ACTIVE'
: 'Live status: VPN is not active',
style: Theme.of(context).textTheme.titleMedium,
),
),
const SizedBox(height: 8),
Text(
'Use the buttons below to preview each presentation '
'regardless of your real VPN status.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 24),
Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
FilledButton(
onPressed: () =>
VpnNoticeBanner.show(context, style: _style),
child: const Text('Show banner'),
),
FilledButton(
onPressed: () =>
VpnNoticeSnackBar.show(context, style: _style),
child: const Text('Show snack bar'),
),
FilledButton(
onPressed: () =>
VpnNoticeDialog.show(context, style: _style),
child: const Text('Show dialog'),
),
FilledButton(
onPressed: () =>
VpnNoticeBottomSheet.show(context, style: _style),
child: const Text('Show bottom sheet'),
),
FilledButton(
onPressed: _showCustomNotice,
child: const Text('Show custom notice'),
),
],
),
],
),
),
),
),
);
}
}
class _CustomVpnNotice extends StatelessWidget {
const _CustomVpnNotice({required this.onDismiss});
static const _horizontalInset = 16.0;
static const _topInset = 12.0;
static const _contentPadding = EdgeInsets.all(16);
static const _borderRadius = BorderRadius.all(Radius.circular(12));
final VoidCallback onDismiss;
@override
Widget build(BuildContext context) {
return Positioned(
top: MediaQuery.paddingOf(context).top + _topInset,
left: _horizontalInset,
right: _horizontalInset,
child: Material(
color: Colors.black,
borderRadius: _borderRadius,
child: Padding(
padding: _contentPadding,
child: Row(
children: [
const Icon(Icons.vpn_lock_outlined, color: Colors.white),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Custom VPN notice',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
Text(
'This shared layout is identical on every platform.',
style: TextStyle(color: Colors.white),
),
],
),
),
TextButton(
onPressed: onDismiss,
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: const Text('Close'),
),
],
),
),
),
);
}
}