Initial commit (Security Hardened Slate)
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.java-version
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains configure files for the Visual Studio Code
|
||||
# editor. Although we may keep it, we often ignore it.
|
||||
.vscode/
|
||||
|
||||
# Python cache & User files
|
||||
TECHNICAL_DEBT.md
|
||||
RUNNING.md
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# App Config (Hide real IPs/IDs from GitHub)
|
||||
lib/logic/app_config.dart
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
**/build/
|
||||
local_packages/*/build/
|
||||
local_packages/*/.dart_tool/
|
||||
*.dart_tool/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
|
||||
**/android/.cxx/
|
||||
**/android/app/build/
|
||||
|
||||
# Keystore files
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
# Crashlytics cache
|
||||
**/android/app/crashlytics-build.properties
|
||||
|
||||
# Secrets
|
||||
android/key.properties
|
||||
lib/logic/backup/
|
||||
00-FULL-REPORT.md
|
||||
server_hivemind_4_0.py
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 esef
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
Privacy Policy
|
||||
Last updated: May 2026
|
||||
PaladinVPN is a free VPN service built and operated by a solo developer.
|
||||
What we collect
|
||||
We collect the minimum required to operate the service:
|
||||
|
||||
A randomly generated device ID created on your phone. This is not linked to your identity in any way.
|
||||
Your active session status and data usage for the current session only. This is stored in memory and deleted when your session ends.
|
||||
|
||||
What we do not collect
|
||||
|
||||
We do not collect your name, email, or any personal information.
|
||||
We do not log your internet traffic or browsing history.
|
||||
We do not sell or share any data with third parties.
|
||||
|
||||
Ads
|
||||
PaladinVPN uses Google AdMob to display rewarded video ads. Google may collect data as part of ad delivery. Please refer to Google's Privacy Policy for details.
|
||||
Your traffic
|
||||
Your internet traffic is routed through our server located in Finland. We do not inspect, log, or store your traffic.
|
||||
Contact
|
||||
If you have any questions, open an issue on the GitHub repository.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
|
||||
# PaladinVPN
|
||||
A free VPN app I built in my spare time as a student with basically no prior experience.
|
||||
|
||||
There are hundreds of free VPN apps on the Play Store. Most of them are fine honestly — some are genuinely good. But a lot of them are vague about how they work, who runs them, and what happens to your traffic. I wanted to build something where the answer to all of those questions is just... public.
|
||||
|
||||
So here it is.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
You watch a short ad. You get 60 minutes and 2GB of full-speed traffic. That's it. The ad pays for the server (actually not even enough). No accounts, no emails, no subscriptions.
|
||||
|
||||
When your data runs out you get throttled to 1.5 Mbps instead of getting cut off. You can watch a bonus ad to top up anytime.
|
||||
|
||||
---
|
||||
|
||||
## The stack
|
||||
- **App** — Flutter (Android only for now)
|
||||
- **Server** — A single Debian box rented from Hetzner, located in Finland
|
||||
- **VPN protocol** — AmneziaWG, which is WireGuard with obfuscation so it works on carriers that block standard WireGuard
|
||||
- **Backend** — A Python script I wrote called Hivemind that manages sessions, data quotas, and throttling
|
||||
- **Ads** — Google AdMob rewarded ads, verified server-side so fake callbacks don't work
|
||||
|
||||
---
|
||||
|
||||
## Limitations (being honest)
|
||||
- One server, one location (Finland). That's all I can afford right now
|
||||
- 2 cores and 4GB RAM. It will not handle thousands of concurrent users
|
||||
- iOS is not supported and probably won't be for a while
|
||||
- Countries with very aggressive DPI (China, Russia, etc.) may still have issues
|
||||
- I'm a student doing this in my free time. Expect rough edges
|
||||
|
||||
---
|
||||
|
||||
## Why I built this
|
||||
Free time project. Wanted to learn how VPNs actually work under the hood. Ended up building the whole thing from scratch — the Flutter app, the AmneziaWG setup, the session management backend, the AdMob integration. It took way longer than expected.
|
||||
|
||||
---
|
||||
|
||||
## Privacy
|
||||
Your traffic goes through my server in Finland. I don't log it, I don't sell it, I have no reason to. The session manager (Hivemind) only tracks whether your session is active and how much data you've used — nothing else.
|
||||
|
||||
Internet privacy at the cost of watching an ad. Honest business.
|
||||
|
||||
You can read the server code yourself, it's in this repo.
|
||||
|
||||
---
|
||||
|
||||
*PaladinVPN is not affiliated with any VPN company.*
|
||||
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
@@ -0,0 +1,61 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.paladinvpn.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.add("META-INF/**")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.paladinvpn.app"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { rootProject.file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,48 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<!-- Required by url_launcher on Android 11+ to open external browser -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="Paladin VPN"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="false"
|
||||
android:allowBackup="false">
|
||||
|
||||
<!-- AdMob App ID (real ID) -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||
android:value="ca-app-pub-5671224884648691~1007152454"/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.paladinvpn.app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/black" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 547 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@android:color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,30 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.11.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.layout.buildDirectory
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.esefcloud.vpn"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.esefcloud.vpn"
|
||||
minSdk = 21
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"META-INF/versions/9/OSGI-INF/MANIFEST.MF"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
@@ -0,0 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
@@ -0,0 +1,25 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.11.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 547 KiB |
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||
import 'package:paladinvpn/components/online_dot.dart';
|
||||
|
||||
/// 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.
|
||||
class BottomBar extends StatelessWidget {
|
||||
const BottomBar({super.key});
|
||||
|
||||
// Dark card background color for high contrast overlay
|
||||
static const Color _cardBg = Color(0xFF1E2533);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<VpnConnection>(
|
||||
builder: (context, vpn, _) {
|
||||
final isOnline = vpn.status == VpnStatus.connected;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: _cardBg,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(74, 85, 104, 0.15),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Displays the server online/offline chip
|
||||
OnlineDot(isOnline: isOnline),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:paladinvpn/logic/session_timer.dart';
|
||||
|
||||
/// ClockDisplay shows a large circular progress ring and the session countdown timer.
|
||||
///
|
||||
/// The timer text glows electric cyan when a session is active and dims to slate gray when idle.
|
||||
class ClockDisplay extends StatelessWidget {
|
||||
const ClockDisplay({super.key});
|
||||
|
||||
static const Color _cyanGlow = Color(0xFF00E5FF);
|
||||
static const Color _dimSlate = Color(0xFF4A5568);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SessionTimer>(
|
||||
builder: (context, timer, _) {
|
||||
final isLive = timer.isRunning;
|
||||
final color = isLive ? _cyanGlow : _dimSlate;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 220,
|
||||
height: 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background track ring representing the maximum capacity of the countdown
|
||||
const CustomPaint(
|
||||
size: Size(220, 220),
|
||||
painter: _ArcPainter(
|
||||
progress: 1.0,
|
||||
color: Color.fromRGBO(74, 85, 104, 0.2),
|
||||
strokeWidth: 4,
|
||||
),
|
||||
),
|
||||
|
||||
// Live progress ring showing current remaining time relative to the session limit
|
||||
if (isLive)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: timer.progress),
|
||||
duration: const Duration(milliseconds: 400),
|
||||
builder: (context, value, _) => CustomPaint(
|
||||
size: const Size(220, 220),
|
||||
painter: _ArcPainter(
|
||||
progress: value,
|
||||
color: _cyanGlow,
|
||||
strokeWidth: 4,
|
||||
glow: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 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 {
|
||||
final double progress;
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
final bool glow;
|
||||
|
||||
const _ArcPainter({
|
||||
required this.progress,
|
||||
required this.color,
|
||||
this.strokeWidth = 4,
|
||||
this.glow = false,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height)
|
||||
.deflate(strokeWidth / 2);
|
||||
|
||||
// Optional blurred background paint to create an electric neon glow effect
|
||||
if (glow) {
|
||||
final glowPaint = Paint()
|
||||
..color = const Color.fromRGBO(0, 229, 255, 0.15)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth + 8
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8);
|
||||
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint);
|
||||
}
|
||||
|
||||
// Core progress stroke paint
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ArcPainter old) =>
|
||||
old.progress != progress || old.color != color;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||
import 'package:paladinvpn/logic/session_timer.dart';
|
||||
|
||||
class ConnectButton extends StatefulWidget {
|
||||
const ConnectButton({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectButton> createState() => _ConnectButtonState();
|
||||
}
|
||||
|
||||
class _ConnectButtonState extends State<ConnectButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _pulseController;
|
||||
late Animation<double> _pulseAnim;
|
||||
|
||||
static const Color _cyanGlow = Color(0xFF00E5FF);
|
||||
static const Color _bgDark = Color(0xFF1A202C);
|
||||
|
||||
bool _inCooldown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
_pulseAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() async {
|
||||
if (_inCooldown) return;
|
||||
|
||||
setState(() {
|
||||
_inCooldown = true;
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_inCooldown = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
final vpn = context.read<VpnConnection>();
|
||||
final timer = context.read<SessionTimer>();
|
||||
|
||||
if (vpn.status == VpnStatus.connected) {
|
||||
vpn.disconnect();
|
||||
timer.stop();
|
||||
_pulseController.stop();
|
||||
_pulseController.reset();
|
||||
} else if (vpn.status == VpnStatus.disconnected ||
|
||||
vpn.status == VpnStatus.error) {
|
||||
final success = await vpn.connect();
|
||||
if (success) {
|
||||
timer.start('main_ad');
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer2<VpnConnection, SessionTimer>(
|
||||
builder: (context, vpn, timer, _) {
|
||||
final isConnected = vpn.status == VpnStatus.connected;
|
||||
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(
|
||||
onTap: (isConnecting || _inCooldown) ? null : _handleTap,
|
||||
child: AnimatedBuilder(
|
||||
animation: _pulseAnim,
|
||||
builder: (context, child) {
|
||||
final glowRadius = isConnected ? 20.0 + (_pulseAnim.value * 15.0) : 0.0;
|
||||
final glowAlpha = isConnected
|
||||
? ((0.3 + _pulseAnim.value * 0.2) * 255).round()
|
||||
: 0;
|
||||
|
||||
return Container(
|
||||
width: 260,
|
||||
height: 260,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _bgDark,
|
||||
border: Border.all(
|
||||
color: _inCooldown
|
||||
? Colors.grey.withValues(alpha: 0.5)
|
||||
: isConnected
|
||||
? _cyanGlow
|
||||
: isConnecting
|
||||
? const Color.fromRGBO(0, 229, 255, 0.5)
|
||||
: const Color.fromRGBO(74, 85, 104, 0.5),
|
||||
width: isConnected && !_inCooldown ? 3 : 2,
|
||||
),
|
||||
boxShadow: isConnected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(glowAlpha, 0, 229, 255),
|
||||
blurRadius: glowRadius,
|
||||
spreadRadius: glowRadius * 0.3,
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
child: Center(
|
||||
child: isConnected
|
||||
? _buildConnectedStats(timer)
|
||||
: _buildDisconnectedIcon(vpn.status, _inCooldown),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown) {
|
||||
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
|
||||
return const SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Color.fromRGBO(0, 229, 255, 0.7),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
inCooldown ? Icons.hourglass_bottom : Icons.power_settings_new,
|
||||
size: 64,
|
||||
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.7),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
inCooldown ? 'PLEASE WAIT' : 'TAP TO CONNECT',
|
||||
style: TextStyle(
|
||||
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.9),
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Header displays the Paladin brand logo, title, and subtitle.
|
||||
///
|
||||
/// It represents the top branding portion of the main dashboard and is designed
|
||||
/// with clean proportions, consistent typography, and a modern cyberpunk-inspired color scheme.
|
||||
class Header extends StatelessWidget {
|
||||
const Header({super.key});
|
||||
|
||||
// Cyber-paladin cyan glow accent color used as a fallback for the shield icon
|
||||
static const Color _cyanGlow = Color(0xFF00E5FF);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Brand logo image with a fallback Icon in case the asset is missing
|
||||
Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 120, // Enlarged logo to stand out visually
|
||||
height: 120,
|
||||
errorBuilder: (context, error, stackTrace) => const Icon(
|
||||
Icons.shield,
|
||||
size: 120,
|
||||
color: _cyanGlow,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8), // Standard padding between the logo and the title
|
||||
|
||||
// Brand primary name
|
||||
const Text(
|
||||
'Paladin',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 8,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2), // Close spacing for secondary information
|
||||
|
||||
// Brand secondary label / purpose
|
||||
const Text(
|
||||
'SECURE TUNNEL',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 4,
|
||||
color: Color(0xAA4A5568), // Semi-transparent slate gray
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class InfoButton extends StatelessWidget {
|
||||
const InfoButton({Key? key}) : super(key: key);
|
||||
|
||||
Future<void> _launchGitHub() async {
|
||||
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
|
||||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||
debugPrint('Could not launch $url');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.help_outline, color: Colors.white, size: 28),
|
||||
onPressed: _launchGitHub,
|
||||
tooltip: 'About PaladinVPN',
|
||||
splashRadius: 24,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// OnlineDot displays a glowing green dot when the server is online/connected,
|
||||
/// and a muted gray dot when disconnected.
|
||||
class OnlineDot extends StatelessWidget {
|
||||
/// Represents if the VPN tunnel session is active (online).
|
||||
final bool isOnline;
|
||||
|
||||
const OnlineDot({
|
||||
super.key,
|
||||
required this.isOnline,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Glowing status dot: Green when active, muted gray when inactive.
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isOnline ? const Color(0xFF00E676) : const Color(0xFF4A5568),
|
||||
boxShadow: isOnline
|
||||
? const [
|
||||
BoxShadow(
|
||||
color: Color.fromRGBO(0, 230, 118, 0.5),
|
||||
blurRadius: 6,
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Dynamic status text label
|
||||
Text(
|
||||
isOnline ? 'Server Online' : 'Disconnected',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isOnline
|
||||
? const Color.fromRGBO(255, 255, 255, 0.8)
|
||||
: const Color(0xFF4A5568),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class PrivacyPolicyButton extends StatelessWidget {
|
||||
const PrivacyPolicyButton({super.key});
|
||||
|
||||
Future<void> _launchPrivacyPolicy() async {
|
||||
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN/blob/main/PRIVACY_POLICY.md');
|
||||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||
debugPrint('Could not launch $url');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.privacy_tip_outlined, color: Colors.white, size: 28),
|
||||
onPressed: _launchPrivacyPolicy,
|
||||
tooltip: 'Privacy Policy',
|
||||
splashRadius: 24,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||
|
||||
/// StatusText displays the status of the VPN tunnel.
|
||||
///
|
||||
/// When connected, it shows the Finnish flag along with the tunnel location ("Helsinki, Finland").
|
||||
/// When connecting or disconnected, it shows status messages with clean micro-animations.
|
||||
class StatusText extends StatelessWidget {
|
||||
const StatusText({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<VpnConnection>(
|
||||
builder: (context, vpn, _) {
|
||||
final isConnected = vpn.status == VpnStatus.connected;
|
||||
final isConnecting = vpn.status == VpnStatus.connecting;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
key: ValueKey(vpn.status),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x22FFFFFF), // Subtle glass effect background
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isConnected
|
||||
? const Color(0x8800E5FF) // Glowing cyan border when connected
|
||||
: const Color(0x33FFFFFF), // Muted translucent border when disconnected/connecting
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isConnected) ...[
|
||||
// Circular Finnish flag icon representing the active tunnel endpoint
|
||||
Image.asset(
|
||||
'assets/finland_flag_256.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Helsinki, Finland',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF00E5FF),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Dynamic status icon (spinning arrows for loading, outlined shield for idle)
|
||||
Icon(
|
||||
isConnecting ? Icons.autorenew : Icons.shield_outlined,
|
||||
size: 18,
|
||||
color: isConnecting
|
||||
? const Color(0xFF00E5FF)
|
||||
: const Color(0xAA4A5568),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
vpn.statusMessage.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: isConnecting
|
||||
? const Color(0xFF00E5FF)
|
||||
: const Color(0xAA4A5568),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:paladinvpn/logic/session_timer.dart';
|
||||
import 'package:paladinvpn/logic/ad_manager.dart';
|
||||
|
||||
/// WatchAdButton is a voluntary option allowing the user to watch a rewarded video ad
|
||||
/// and extend their active VPN tunnel session by 30 minutes as a reward.
|
||||
class WatchAdButton extends StatelessWidget {
|
||||
const WatchAdButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () async {
|
||||
final success = await context.read<AdManager>().showAd('bonus_ad');
|
||||
if (success && context.mounted) {
|
||||
context.read<SessionTimer>().addBonusTime(); // Calls Hivemind API for 30m + 1GB
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'+30 minutes added!',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: const Color(0xFF006064),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0x1A00E5FF), Color(0x0D00E5FF)],
|
||||
),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(0, 229, 255, 0.25),
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('❤️', style: TextStyle(fontSize: 14)),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Support +30m',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF00E5FF),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
import 'package:paladinvpn/logic/crypto_service.dart';
|
||||
import 'package:paladinvpn/logic/hivemind_service.dart';
|
||||
import 'package:paladinvpn/logic/app_config.dart';
|
||||
|
||||
/// AdManager manages the lifecycle of rewarded video ads.
|
||||
///
|
||||
/// It acts as a strict gatekeeper: users MUST watch an ad to connect.
|
||||
/// If an ad is not ready when they click connect, it forces them to wait
|
||||
/// while it loads. If they dismiss the ad early, they do not get the connection.
|
||||
class AdManager extends ChangeNotifier {
|
||||
RewardedAd? _rewardedAd;
|
||||
|
||||
bool _isAdLoaded = false;
|
||||
bool get isAdLoaded => _isAdLoaded;
|
||||
|
||||
bool _isAdLoading = false;
|
||||
bool get isAdLoading => _isAdLoading;
|
||||
|
||||
Completer<bool>? _loadCompleter;
|
||||
|
||||
// Google-provided test ad unit
|
||||
static String get _adUnitId => AppConfig.adUnitId;
|
||||
|
||||
AdManager() {
|
||||
_preloadAd();
|
||||
}
|
||||
|
||||
/// Internal helper to load an ad and return a Future when it's ready
|
||||
Future<bool> _preloadAd() async {
|
||||
// If already loaded, return instantly
|
||||
if (_isAdLoaded) return true;
|
||||
|
||||
// If we're already loading one right now, wait for that same request to finish
|
||||
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;
|
||||
}
|
||||
|
||||
/// Displays the rewarded video ad to the user.
|
||||
Future<bool> showAd(String adType) async {
|
||||
// 1. Ensure the ad is fully loaded before trying to show it
|
||||
if (!_isAdLoaded || _rewardedAd == null) {
|
||||
final loaded = await _preloadAd();
|
||||
if (!loaded || _rewardedAd == null) {
|
||||
debugPrint('[AdManager] Cannot show ad, failed to load.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch device credentials for Server-Side Verification (SSV)
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final keys = await CryptoService.getOrCreateKeys();
|
||||
final pubKey = keys['publicKey']!;
|
||||
|
||||
// 3. Attach custom data to the ad so Google sends it back to our Hivemind server
|
||||
_rewardedAd!.serverSideVerificationOptions = ServerSideVerificationOptions(
|
||||
customData: jsonEncode({
|
||||
'device_id': deviceId,
|
||||
'public_key': pubKey,
|
||||
'ad_type': adType,
|
||||
}),
|
||||
);
|
||||
|
||||
Completer<bool> rewardCompleter = Completer<bool>();
|
||||
|
||||
// 4. Setup full screen callbacks to handle dismissal and errors
|
||||
_rewardedAd!.fullScreenContentCallback = FullScreenContentCallback(
|
||||
onAdShowedFullScreenContent: (ad) => debugPrint('[AdManager] Ad showing.'),
|
||||
onAdDismissedFullScreenContent: (ad) {
|
||||
debugPrint('[AdManager] Ad dismissed.');
|
||||
ad.dispose();
|
||||
_isAdLoaded = false;
|
||||
_rewardedAd = null;
|
||||
// Start loading the next ad immediately
|
||||
_preloadAd();
|
||||
if (!rewardCompleter.isCompleted) {
|
||||
rewardCompleter.complete(false); // User closed before earning reward
|
||||
}
|
||||
},
|
||||
onAdFailedToShowFullScreenContent: (ad, error) {
|
||||
debugPrint('[AdManager] Ad failed to show: $error');
|
||||
ad.dispose();
|
||||
_isAdLoaded = false;
|
||||
_rewardedAd = null;
|
||||
if (!rewardCompleter.isCompleted) {
|
||||
rewardCompleter.complete(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 5. Show the ad
|
||||
await _rewardedAd!.show(
|
||||
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
|
||||
debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}');
|
||||
// The SSV callback will hit our server independently. We just need to signal the UI
|
||||
// that the ad was successfully completed.
|
||||
if (!rewardCompleter.isCompleted) {
|
||||
rewardCompleter.complete(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return rewardCompleter.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rewardedAd?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// =============================================================================
|
||||
// app_config.example.dart — TEMPLATE FOR SERVER SETTINGS
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Rename this file to `app_config.dart` and fill in your real values below.
|
||||
// Do not commit your real `app_config.dart` to GitHub!
|
||||
// =============================================================================
|
||||
|
||||
class AppConfig {
|
||||
// ── STEP 1: Paste your Hetzner server IP here ─────────────────────────────
|
||||
static const String serverIp = '127.0.0.1'; // REPLACE WITH REAL IP
|
||||
|
||||
// ── STEP 2: WireGuard UDP traffic port (default: 51820) ───────────────────
|
||||
static const String wgPort = '4433';
|
||||
|
||||
// ── STEP 3: AmneziaWG DPI Obfuscation Settings ───────────────────────────
|
||||
// These parameters must match exactly what your amnezia-wg-easy server expects.
|
||||
static const int awgJc = 4;
|
||||
static const int awgJmin = 20;
|
||||
static const int awgJmax = 100;
|
||||
static const int awgS1 = 15;
|
||||
static const int awgS2 = 23;
|
||||
static const int awgH1 = 1;
|
||||
static const int awgH2 = 2;
|
||||
static const int awgH3 = 3;
|
||||
static const int awgH4 = 4;
|
||||
|
||||
// ── STEP 4: AdMob Rewarded Video ID ──────────────────────────────────────
|
||||
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE WITH REAL AD UNIT
|
||||
|
||||
// ── STEP 5: Network & Tunnel Settings ────────────────────────────────────
|
||||
static const String dnsServers = '1.1.1.1, 1.0.0.1';
|
||||
static const String allowedIps = '0.0.0.0/0, ::/0';
|
||||
static const int mtu = 1280;
|
||||
static const int persistentKeepalive = 25;
|
||||
static const int hivemindPort = 5000;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Nothing below this line needs to be changed.
|
||||
// ── STEP 1: Hivemind Server API ──────────────────────────────────────────
|
||||
static String get hivemindApiBase => 'https://yourdomain.duckdns.org/api'; // REPLACE WITH REAL DOMAIN
|
||||
|
||||
/// The full server address passed to the WireGuard tunnel (IP:port).
|
||||
static String get serverEndpoint => '$serverIp:$wgPort';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class CryptoService {
|
||||
static const String _privKeyPref = 'wg_private_key';
|
||||
static const String _pubKeyPref = 'wg_public_key';
|
||||
static const String _deviceIdPref = 'device_uuid';
|
||||
|
||||
static const _storage = FlutterSecureStorage();
|
||||
|
||||
// ── Get or create the persistent device UUID ──────────────────────────────
|
||||
static Future<String> getDeviceId() async {
|
||||
final existing = await _storage.read(key: _deviceIdPref);
|
||||
if (existing != null) return existing;
|
||||
|
||||
final newId = const Uuid().v4();
|
||||
await _storage.write(key: _deviceIdPref, value: newId);
|
||||
return newId;
|
||||
}
|
||||
|
||||
// ── Get or create the X25519 keypair for WireGuard ────────────────────────
|
||||
static Future<Map<String, String>> getOrCreateKeys() async {
|
||||
String? privBase64 = await _storage.read(key: _privKeyPref);
|
||||
String? pubBase64 = await _storage.read(key: _pubKeyPref);
|
||||
|
||||
if (privBase64 != null && pubBase64 != null) {
|
||||
return {'privateKey': privBase64, 'publicKey': pubBase64};
|
||||
}
|
||||
|
||||
final algorithm = X25519();
|
||||
final keyPair = await algorithm.newKeyPair();
|
||||
|
||||
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
|
||||
final publicKey = await keyPair.extractPublicKey();
|
||||
|
||||
privBase64 = base64Encode(privateKeyBytes);
|
||||
pubBase64 = base64Encode(publicKey.bytes);
|
||||
|
||||
await _storage.write(key: _privKeyPref, value: privBase64);
|
||||
await _storage.write(key: _pubKeyPref, value: pubBase64);
|
||||
|
||||
return {'privateKey': privBase64, 'publicKey': pubBase64};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:paladinvpn/logic/app_config.dart';
|
||||
import 'package:paladinvpn/logic/crypto_service.dart';
|
||||
|
||||
class HivemindService {
|
||||
/// Polls the Hivemind server until the session is ready or timeout occurs.
|
||||
/// Returns the WireGuard config string.
|
||||
static Future<String> fetchConfigWithPolling() async {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final keys = await CryptoService.getOrCreateKeys();
|
||||
final privKey = keys['privateKey']!;
|
||||
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||
|
||||
// ── TEMPORARY AD BYPASS HACK FOR TESTING ──
|
||||
// Since you disabled the crypto signature check on the Python server,
|
||||
// we can just send a fake "Ad Watched" ping from the app directly.
|
||||
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: 2));
|
||||
} catch (_) {}
|
||||
// ──────────────────────────────────────────
|
||||
|
||||
int attempts = 0;
|
||||
while (attempts < 15) {
|
||||
try {
|
||||
final response = await http.get(url).timeout(const Duration(seconds: 3));
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
// Debugging log to see exactly what the server returned
|
||||
print('[HivemindService] Polling response: ${response.body}');
|
||||
|
||||
if (data['active'] == true && data['client_ip'] != null && data['server_pubkey'] != null) {
|
||||
final clientIp = data['client_ip'];
|
||||
final serverPubkey = data['server_pubkey'];
|
||||
|
||||
// Construct the WireGuard config locally
|
||||
// I CHANGED IPS MAY BREAK STUFF
|
||||
final configText = '''
|
||||
[Interface]
|
||||
PrivateKey = $privKey
|
||||
Address = $clientIp/24
|
||||
DNS = ${AppConfig.dnsServers}
|
||||
MTU = ${AppConfig.mtu}
|
||||
Jc = ${AppConfig.awgJc}
|
||||
Jmin = ${AppConfig.awgJmin}
|
||||
Jmax = ${AppConfig.awgJmax}
|
||||
S1 = ${AppConfig.awgS1}
|
||||
S2 = ${AppConfig.awgS2}
|
||||
H1 = ${AppConfig.awgH1}
|
||||
H2 = ${AppConfig.awgH2}
|
||||
H3 = ${AppConfig.awgH3}
|
||||
H4 = ${AppConfig.awgH4}
|
||||
|
||||
[Peer]
|
||||
PublicKey = $serverPubkey
|
||||
AllowedIPs = ${AppConfig.allowedIps}
|
||||
Endpoint = ${AppConfig.serverEndpoint}
|
||||
PersistentKeepalive = ${AppConfig.persistentKeepalive}
|
||||
''';
|
||||
return configText;
|
||||
}
|
||||
} else {
|
||||
print('[HivemindService] Non-200 status: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('[HivemindService] Polling exception: $e');
|
||||
// Ignore network errors and keep polling
|
||||
}
|
||||
|
||||
attempts++;
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
}
|
||||
|
||||
throw Exception("Session not activated. S2S callback failed or timed out.");
|
||||
}
|
||||
|
||||
/// Disconnects the session (cleans up on server)
|
||||
static Future<void> disconnectAndCleanup() async {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop');
|
||||
try {
|
||||
await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'device_id': deviceId}),
|
||||
).timeout(const Duration(seconds: 3));
|
||||
} catch (_) {
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:paladinvpn/logic/app_config.dart';
|
||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||
import 'package:paladinvpn/logic/crypto_service.dart';
|
||||
|
||||
/// SessionTimer acts as the client-side session authority for the VPN tunnel.
|
||||
///
|
||||
/// It connects to the Hivemind API on the server to sync the true remaining
|
||||
/// time, data quotas, speed, and throttling status.
|
||||
class SessionTimer extends ChangeNotifier {
|
||||
Timer? _timer;
|
||||
int _tickCount = 0;
|
||||
|
||||
final VpnConnection vpnConnection;
|
||||
|
||||
// ── Session State ──
|
||||
int _remainingSeconds = 0;
|
||||
int _quotaBytes = 0;
|
||||
int _usedBytes = 0;
|
||||
bool _isThrottled = false;
|
||||
|
||||
// ── Speed State ──
|
||||
int _lastUsedBytes = 0;
|
||||
double _currentSpeedKbps = 0.0; // Total speed (RX+TX)
|
||||
|
||||
SessionTimer({required this.vpnConnection});
|
||||
|
||||
int get remaining => _remainingSeconds;
|
||||
bool get isRunning => _timer != null && _timer!.isActive;
|
||||
bool get isExpired => _remainingSeconds <= 0 && !isRunning;
|
||||
|
||||
int get quotaBytes => _quotaBytes;
|
||||
int get usedBytes => _usedBytes;
|
||||
int get remainingBytes => _quotaBytes > _usedBytes ? _quotaBytes - _usedBytes : 0;
|
||||
bool get isThrottled => _isThrottled;
|
||||
double get currentSpeedKbps => _currentSpeedKbps;
|
||||
|
||||
String get formatted {
|
||||
final h = (_remainingSeconds ~/ 3600).toString().padLeft(2, '0');
|
||||
final m = ((_remainingSeconds % 3600) ~/ 60).toString().padLeft(2, '0');
|
||||
final s = (_remainingSeconds % 60).toString().padLeft(2, '0');
|
||||
return '$h:$m:$s';
|
||||
}
|
||||
|
||||
String get formattedDataRemaining {
|
||||
if (_quotaBytes == 0) return '0.00 GB';
|
||||
final gb = remainingBytes / (1024 * 1024 * 1024);
|
||||
return '${gb.toStringAsFixed(2)} GB';
|
||||
}
|
||||
|
||||
double get progress {
|
||||
// Max duration is generally 60 or 90 minutes. We'll base the ring purely on data usage or just standard time
|
||||
// For UI simplicity, we'll return data progress:
|
||||
if (_quotaBytes == 0) return 0.0;
|
||||
return (_quotaBytes - _usedBytes) / _quotaBytes;
|
||||
}
|
||||
|
||||
Future<void> start(String adType) async {
|
||||
_remainingSeconds = 3600; // optimistic start
|
||||
_quotaBytes = 2 * 1024 * 1024 * 1024;
|
||||
_usedBytes = 0;
|
||||
_lastUsedBytes = 0;
|
||||
_currentSpeedKbps = 0.0;
|
||||
_isThrottled = false;
|
||||
_tickCount = 0;
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), _tick);
|
||||
|
||||
// Kick off an immediate sync so the UI shows real data right away
|
||||
_syncWithHivemind();
|
||||
}
|
||||
|
||||
void _tick(Timer t) {
|
||||
if (_remainingSeconds > 0) {
|
||||
_remainingSeconds--;
|
||||
}
|
||||
|
||||
_tickCount++;
|
||||
// Poll the server every 3 seconds to get live stats
|
||||
if (_tickCount % 3 == 0) {
|
||||
_syncWithHivemind();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _syncWithHivemind() async {
|
||||
try {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||
final response = await http.get(url).timeout(const Duration(seconds: 3));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
final bool active = data['active'] ?? false;
|
||||
if (!active) {
|
||||
// Server killed the session!
|
||||
stop();
|
||||
vpnConnection.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
_remainingSeconds = data['expires_in_seconds'] ?? _remainingSeconds;
|
||||
_quotaBytes = data['quota_bytes'] ?? _quotaBytes;
|
||||
_usedBytes = data['used_bytes'] ?? _usedBytes;
|
||||
_isThrottled = data['is_throttled'] ?? false;
|
||||
|
||||
// Calculate speed over the polling interval
|
||||
final int deltaBytes = _usedBytes - _lastUsedBytes;
|
||||
if (_lastUsedBytes > 0 && deltaBytes > 0) {
|
||||
_currentSpeedKbps = (deltaBytes / 3) / 1024;
|
||||
} else {
|
||||
_currentSpeedKbps = 0.0;
|
||||
}
|
||||
_lastUsedBytes = _usedBytes;
|
||||
|
||||
notifyListeners();
|
||||
} else if (response.statusCode == 404) {
|
||||
// Peer not found on server (expired/deleted natively)
|
||||
stop();
|
||||
vpnConnection.disconnect();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Hivemind sync error: $e');
|
||||
// If we can't reach the server, just let the local timer run
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addBonusTime() async {
|
||||
final ip = vpnConnection.internalIp;
|
||||
if (ip != null) {
|
||||
try {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
await http.post(
|
||||
Uri.parse('${AppConfig.hivemindApiBase}/session/start'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'ip': ip,
|
||||
'device_id': deviceId,
|
||||
'ad_type': 'bonus_ad'
|
||||
}),
|
||||
).timeout(const Duration(seconds: 5));
|
||||
|
||||
// Immediately sync to fetch the updated limits and un-throttled status
|
||||
_syncWithHivemind();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to add bonus time with Hivemind: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_remainingSeconds = 0;
|
||||
_currentSpeedKbps = 0.0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// =============================================================================
|
||||
// vpn_connection.dart — WireGuard Tunnel Manager
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// This file manages the WireGuard tunnel on the Android device.
|
||||
// It is the bridge between the UI (the big connect button) and the actual
|
||||
// VPN tunnel running inside the Android operating system.
|
||||
//
|
||||
// HOW IT WORKS:
|
||||
// 1. When connect() is called, it first asks WgApiService to fetch the config
|
||||
// file for this device from the server (see wg_api_service.dart).
|
||||
// 2. It passes that config text to the wireguard_flutter plugin, which handles
|
||||
// all the native Android VPN stuff automatically.
|
||||
// 3. It listens to the native tunnel state and updates the UI accordingly.
|
||||
//
|
||||
// You never need to edit this file.
|
||||
// Server settings → app_config.dart
|
||||
// Server communication → wg_api_service.dart
|
||||
// =============================================================================
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:wireguard_flutter/wireguard_flutter.dart';
|
||||
import 'package:paladinvpn/logic/app_config.dart';
|
||||
import 'package:paladinvpn/logic/hivemind_service.dart';
|
||||
|
||||
// ── VPN States ────────────────────────────────────────────────────────────────
|
||||
// These are the possible states the tunnel can be in.
|
||||
// The UI uses these to decide what to display (spinner, shield icon, etc.)
|
||||
enum VpnStatus {
|
||||
disconnected, // No tunnel active — idle state
|
||||
connecting, // Fetching config from server + starting tunnel
|
||||
connected, // Tunnel is up and routing traffic
|
||||
disconnecting, // Tearing down the tunnel
|
||||
error, // Something went wrong (server unreachable, etc.)
|
||||
}
|
||||
|
||||
// ── VpnConnection ─────────────────────────────────────────────────────────────
|
||||
// This is a "ChangeNotifier" — it works like a Python observable/event emitter.
|
||||
// Whenever the state changes, it automatically notifies all widgets that are
|
||||
// listening, and they re-render themselves. You never call setState() manually.
|
||||
class VpnConnection extends ChangeNotifier {
|
||||
// Current tunnel state (starts disconnected)
|
||||
VpnStatus _status = VpnStatus.disconnected;
|
||||
VpnStatus get status => _status;
|
||||
|
||||
// Short message shown to the user under the connect button
|
||||
String _statusMessage = 'Tap to connect';
|
||||
String get statusMessage => _statusMessage;
|
||||
|
||||
// Optional error message shown if something goes wrong
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
// The internal WireGuard IP assigned to this device (e.g. 10.8.0.2)
|
||||
// Populated after a successful config fetch and used by SessionTimer.
|
||||
String? internalIp;
|
||||
|
||||
// The wireguard_flutter plugin instance — this is what actually talks to Android
|
||||
final _wireguard = WireGuardFlutter.instance;
|
||||
|
||||
// Subscription to the native tunnel state stream
|
||||
StreamSubscription? _stageSub;
|
||||
|
||||
// ── Initialization ────────────────────────────────────────────────────────
|
||||
// Called automatically when the app starts (via Provider in main.dart)
|
||||
VpnConnection() {
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
// WireGuard doesn't work in a web browser — skip on web
|
||||
if (kIsWeb) return;
|
||||
|
||||
try {
|
||||
// Register our tunnel interface with Android.
|
||||
// 'Paladin0' is just the name of the network interface — like "eth0"
|
||||
await _wireguard.initialize(interfaceName: 'Paladin0');
|
||||
} catch (e) {
|
||||
debugPrint('WireGuard init error (expected on emulator): $e');
|
||||
}
|
||||
|
||||
// Start listening to native tunnel state changes from Android.
|
||||
// When Android reports the tunnel went up/down, _mapStage() is called.
|
||||
try {
|
||||
_stageSub = _wireguard.vpnStageSnapshot.listen(_mapStage);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Native State Mapping ──────────────────────────────────────────────────
|
||||
// Android sends back low-level states like "authenticating", "preparing", etc.
|
||||
// This function translates those into our simpler 5-state enum above.
|
||||
void _mapStage(VpnStage stage) {
|
||||
switch (stage) {
|
||||
case VpnStage.connected:
|
||||
_setStatus(VpnStatus.connected, 'Secured');
|
||||
case VpnStage.disconnected:
|
||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||
case VpnStage.connecting:
|
||||
case VpnStage.waitingConnection:
|
||||
case VpnStage.authenticating:
|
||||
case VpnStage.preparing:
|
||||
case VpnStage.reconnect:
|
||||
_setStatus(VpnStatus.connecting, 'Establishing tunnel…');
|
||||
case VpnStage.disconnecting:
|
||||
case VpnStage.exiting:
|
||||
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
|
||||
default:
|
||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connect ───────────────────────────────────────────────────────────────
|
||||
// This is the main function — called after the user watches the rewarded ad.
|
||||
//
|
||||
// Step 1: Call the WG-Easy API to get the .conf for this device
|
||||
// Step 2: Pass that .conf to the WireGuard plugin to start the tunnel
|
||||
//
|
||||
// Returns true if the connection was successful, false if it failed.
|
||||
// The connect button uses this to decide whether to start the timer.
|
||||
Future<bool> connect() async {
|
||||
// Don't do anything if we're already connected or currently connecting
|
||||
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) return false;
|
||||
|
||||
_setStatus(VpnStatus.connecting, 'Fetching config…');
|
||||
_errorMessage = null;
|
||||
|
||||
// ── Web / emulator fallback ────────────────────────────────────────────
|
||||
if (kIsWeb) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_setStatus(VpnStatus.connected, 'Secured (dev mode)');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Step 2: Fetch config from Hivemind ──
|
||||
String configText;
|
||||
try {
|
||||
configText = await HivemindService.fetchConfigWithPolling();
|
||||
|
||||
// Parse the internal IP (e.g. "Address = 10.8.0.2/24" or "/32")
|
||||
final regex = RegExp(r'Address\s*=\s*([0-9\.]+)/');
|
||||
final match = regex.firstMatch(configText);
|
||||
if (match != null) {
|
||||
internalIp = match.group(1);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Hivemind API error: $e');
|
||||
// Set the exact error as the status message so the user can see it on screen
|
||||
String errorMsg = e.toString().replaceAll('Exception: ', '');
|
||||
if (errorMsg.length > 30) {
|
||||
errorMsg = '${errorMsg.substring(0, 30)}...'; // keep it somewhat short for the UI
|
||||
}
|
||||
_errorMessage = e.toString();
|
||||
_setStatus(VpnStatus.error, errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Step 2: Start the WireGuard tunnel with the fetched config ─────────
|
||||
try {
|
||||
// Re-initialize to ensure VPN permission is granted before starting
|
||||
await _wireguard.initialize(interfaceName: 'Paladin0');
|
||||
await _wireguard.startVpn(
|
||||
serverAddress: AppConfig.serverEndpoint,
|
||||
wgQuickConfig: configText,
|
||||
providerBundleIdentifier: 'com.paladinvpn.app.WireGuardProvider',
|
||||
);
|
||||
_setStatus(VpnStatus.connected, 'Secured');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('WireGuard tunnel error: $e');
|
||||
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
|
||||
_setStatus(VpnStatus.error, 'Tunnel error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Disconnect ────────────────────────────────────────────────────────────
|
||||
// Tears down the active tunnel and returns Android to normal networking.
|
||||
Future<void> disconnect() async {
|
||||
if (_status == VpnStatus.disconnected) return;
|
||||
|
||||
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
|
||||
|
||||
if (kIsWeb) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _wireguard.stopVpn();
|
||||
} catch (e) {
|
||||
debugPrint('WireGuard stop error: $e');
|
||||
}
|
||||
|
||||
// Clean up the peer from the server so we don't leave dead connections lying around
|
||||
await HivemindService.disconnectAndCleanup();
|
||||
|
||||
_setStatus(VpnStatus.disconnected, 'Tap to connect');
|
||||
}
|
||||
|
||||
// ── Internal state updater ────────────────────────────────────────────────
|
||||
// Updates state and triggers a UI re-render (like calling setState() in Python/Qt)
|
||||
void _setStatus(VpnStatus s, String msg) {
|
||||
_status = s;
|
||||
_statusMessage = msg;
|
||||
notifyListeners(); // tells all listening widgets to rebuild
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stageSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:paladinvpn/logic/vpn_connection.dart';
|
||||
import 'package:paladinvpn/logic/session_timer.dart';
|
||||
import 'package:paladinvpn/screens/main_screen.dart';
|
||||
import 'package:paladinvpn/screens/intro.dart';
|
||||
|
||||
/// The entry point for the Paladin VPN application.
|
||||
void main() {
|
||||
// Ensure that the Flutter engine is fully initialized before using platform channels
|
||||
// (e.g., for SystemChrome or MobileAds).
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Lock the application to portrait orientation since the dashboard is a single-screen utility app
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
|
||||
// Configure a stealth system bar layout: transparent status bar with light icons,
|
||||
// blending seamlessly into the dark background gradient.
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Color(0xFF0D1117), // Deep space black
|
||||
systemNavigationBarIconBrightness: Brightness.light,
|
||||
));
|
||||
|
||||
// Inflate the root widget
|
||||
runApp(const PaladinApp());
|
||||
}
|
||||
|
||||
/// The root application widget wrapping the app in necessary state providers and themes.
|
||||
class PaladinApp extends StatelessWidget {
|
||||
const PaladinApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// MultiProvider injects global state controllers down the widget tree.
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
// Manages the WireGuard tunnel lifecycle
|
||||
ChangeNotifierProvider(create: (_) => VpnConnection()),
|
||||
|
||||
// Manages the 1-hour session clock.
|
||||
// Uses ProxyProvider to automatically inject the VpnConnection,
|
||||
// allowing the timer to auto-disconnect the VPN when time expires.
|
||||
ChangeNotifierProxyProvider<VpnConnection, SessionTimer>(
|
||||
create: (ctx) => SessionTimer(
|
||||
vpnConnection: ctx.read<VpnConnection>(),
|
||||
),
|
||||
update: (_, vpn, prev) =>
|
||||
prev ?? SessionTimer(vpnConnection: vpn),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Paladin VPN',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _buildDarkTheme(),
|
||||
home: const IntroScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Constructs the global dark theme used across the application.
|
||||
ThemeData _buildDarkTheme() {
|
||||
const cyanAccent = Color(0xFF00E5FF);
|
||||
const bgDark = Color(0xFF0D1117);
|
||||
|
||||
return ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: bgDark,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: cyanAccent,
|
||||
secondary: cyanAccent,
|
||||
surface: Color(0xFF151C28),
|
||||
),
|
||||
fontFamily: 'Roboto',
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: const Color(0xFF1E2533),
|
||||
contentTextStyle: const TextStyle(color: Colors.white),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
useMaterial3: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paladinvpn/screens/main_screen.dart';
|
||||
|
||||
class IntroScreen extends StatefulWidget {
|
||||
const IntroScreen({super.key});
|
||||
|
||||
@override
|
||||
State<IntroScreen> createState() => _IntroScreenState();
|
||||
}
|
||||
|
||||
class _IntroScreenState extends State<IntroScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Wait for 2.5 seconds then smoothly transition to the main screen
|
||||
Timer(const Duration(milliseconds: 2500), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 800),
|
||||
pageBuilder: (_, __, ___) => const MainScreen(),
|
||||
transitionsBuilder: (_, animation, __, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0D1117), // Deep space black background
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// App Logo
|
||||
Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 140,
|
||||
height: 140,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Fallback icon if logo.png is missing or misconfigured
|
||||
return const Icon(
|
||||
Icons.shield,
|
||||
size: 140,
|
||||
color: Color(0xFF00E5FF),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// PaladinVPN Text
|
||||
const Text(
|
||||
'PALADIN VPN',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// Loading Circle
|
||||
const SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF00E5FF),
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paladinvpn/components/header.dart';
|
||||
import 'package:paladinvpn/components/status_text.dart';
|
||||
import 'package:paladinvpn/components/connect_button.dart';
|
||||
import 'package:paladinvpn/components/bottom_bar.dart';
|
||||
import 'package:paladinvpn/components/info_button.dart';
|
||||
import 'package:paladinvpn/components/privacy_policy_button.dart';
|
||||
|
||||
/// MainScreen serves as the primary home screen for Paladin VPN.
|
||||
///
|
||||
/// It provides a dark mode, cyberpunk-inspired visual layout displaying:
|
||||
/// - Logo and Branding at the top (`Header`)
|
||||
/// - Session countdown timer displaying remaining time (`ClockDisplay`)
|
||||
/// - Tap-to-connect action trigger (`ConnectButton`)
|
||||
/// - Connected status badge displaying tunnel information (`StatusText`)
|
||||
/// - Bottom bar showing server health status and voluntary support button (`BottomBar`)
|
||||
class MainScreen extends StatelessWidget {
|
||||
const MainScreen({super.key});
|
||||
|
||||
// Background base color
|
||||
static const Color _bgTop = Color(0xFF0D1117);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _bgTop,
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: _bgTop,
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/background.png'),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Color(0xAA0D1117), // Deep dark tint overlay to guarantee readability of overlay elements
|
||||
BlendMode.darken,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// ── Main content column ──
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: const IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 4),
|
||||
Header(),
|
||||
Spacer(flex: 1),
|
||||
ConnectButton(),
|
||||
SizedBox(height: 24),
|
||||
StatusText(),
|
||||
Spacer(flex: 3),
|
||||
BottomBar(),
|
||||
SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// ── Info / About buttons (top-right) ──
|
||||
const Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PrivacyPolicyButton(),
|
||||
InfoButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 PT BILLION JAYA GROUP
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,66 @@
|
||||
group 'billion.group.wireguard_flutter'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.8.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
}
|
||||
|
||||
namespace 'billion.group.wireguard_flutter'
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += ['META-INF/**']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.zaneschepke:amneziawg-android:2.3.7'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
|
||||
implementation 'com.beust:klaxon:5.5'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
|
||||
/// to run wireguard on Android 25 or lower
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'wireguard_dart'
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,320 @@
|
||||
package billion.group.wireguard_flutter
|
||||
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
|
||||
import android.app.Activity
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.beust.klaxon.Klaxon
|
||||
import org.amnezia.awg.backend.*
|
||||
import org.amnezia.awg.crypto.Key
|
||||
import org.amnezia.awg.crypto.KeyPair
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
/** WireguardFlutterPlugin */
|
||||
|
||||
const val PERMISSIONS_REQUEST_CODE = 10014
|
||||
const val METHOD_CHANNEL_NAME = "billion.group.wireguard_flutter/wgcontrol"
|
||||
const val METHOD_EVENT_NAME = "billion.group.wireguard_flutter/wgstage"
|
||||
|
||||
class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
|
||||
PluginRegistry.ActivityResultListener {
|
||||
private lateinit var channel: MethodChannel
|
||||
private lateinit var events: EventChannel
|
||||
private lateinit var tunnelName: String
|
||||
private val futureBackend = CompletableDeferred<Backend>()
|
||||
private var vpnStageSink: EventChannel.EventSink? = null
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main.immediate)
|
||||
private var backend: Backend? = null
|
||||
private var havePermission = false
|
||||
private lateinit var context: Context
|
||||
private var activity: Activity? = null
|
||||
private var config: org.amnezia.awg.config.Config? = null
|
||||
private var tunnel: WireGuardTunnel? = null
|
||||
private val TAG = "NVPN"
|
||||
var isVpnChecked = false
|
||||
companion object {
|
||||
private var state: String = "no_connection"
|
||||
|
||||
fun getStatus(): String {
|
||||
return state
|
||||
}
|
||||
}
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
this.havePermission =
|
||||
(requestCode == PERMISSIONS_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)
|
||||
return havePermission
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
|
||||
this.activity = activityPluginBinding.activity as FlutterActivity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
this.activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
|
||||
this.activity = activityPluginBinding.activity as FlutterActivity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
this.activity = null
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
|
||||
events = EventChannel(flutterPluginBinding.binaryMessenger, METHOD_EVENT_NAME)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
backend = createBackend()
|
||||
futureBackend.complete(backend!!)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
channel.setMethodCallHandler(this)
|
||||
events.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
isVpnChecked = false
|
||||
vpnStageSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
isVpnChecked = false
|
||||
vpnStageSink = null
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun createBackend(): Backend {
|
||||
if (backend == null) {
|
||||
backend = GoBackend(context, object : TunnelActionHandler {
|
||||
override fun runPreUp(commands: Collection<String>) {}
|
||||
override fun runPostUp(commands: Collection<String>) {}
|
||||
override fun runPreDown(commands: Collection<String>) {}
|
||||
override fun runPostDown(commands: Collection<String>) {}
|
||||
})
|
||||
}
|
||||
return backend as Backend
|
||||
}
|
||||
|
||||
private fun flutterSuccess(result: Result, o: Any) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
result.success(o)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flutterError(result: Result, error: String) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
result.error(error, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flutterNotImplemented(result: Result) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
|
||||
when (call.method) {
|
||||
"initialize" -> setupTunnel(call.argument<String>("localizedDescription").toString(), result)
|
||||
"start" -> {
|
||||
connect(call.argument<String>("wgQuickConfig").toString(), result)
|
||||
|
||||
if (!isVpnChecked) {
|
||||
if (isVpnActive()) {
|
||||
state = "connected"
|
||||
isVpnChecked = true
|
||||
println("VPN is active")
|
||||
} else {
|
||||
state = "disconnected"
|
||||
isVpnChecked = true
|
||||
println("VPN is not active")
|
||||
}
|
||||
}
|
||||
}
|
||||
"stop" -> {
|
||||
disconnect(result)
|
||||
}
|
||||
"stage" -> {
|
||||
result.success(getStatus())
|
||||
}
|
||||
"checkPermission" -> {
|
||||
checkPermission()
|
||||
result.success(null)
|
||||
}
|
||||
else -> flutterNotImplemented(result)
|
||||
}
|
||||
}
|
||||
private fun isVpnActive(): Boolean {
|
||||
try {
|
||||
val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val activeNetwork = connectivityManager.activeNetwork
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
|
||||
return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "isVpnActive - ERROR - ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
private fun updateStage(stage: String?) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val updatedStage = stage ?: "no_connection"
|
||||
state = updatedStage
|
||||
vpnStageSink?.success(updatedStage.lowercase(Locale.ROOT))
|
||||
}
|
||||
}
|
||||
private fun updateStageFromState(state: Tunnel.State) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
when (state) {
|
||||
Tunnel.State.UP -> updateStage("connected")
|
||||
Tunnel.State.DOWN -> updateStage("disconnected")
|
||||
else -> updateStage("wait_connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun disconnect(result: Result) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (futureBackend.await().getRunningTunnelNames().isEmpty()) {
|
||||
updateStage("disconnected")
|
||||
throw Exception("Tunnel is not running")
|
||||
}
|
||||
updateStage("disconnecting")
|
||||
futureBackend.await().setState(
|
||||
tunnel(tunnelName) { state ->
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Log.i(TAG, "onStateChange - $state")
|
||||
updateStageFromState(state)
|
||||
}
|
||||
}, Tunnel.State.DOWN, config
|
||||
)
|
||||
Log.i(TAG, "Disconnect - success!")
|
||||
flutterSuccess(result, "")
|
||||
} catch (e: BackendException) {
|
||||
Log.e(TAG, "Disconnect - BackendException - ERROR - ${e.reason}", e)
|
||||
flutterError(result, e.reason.toString())
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Disconnect - Can't disconnect from tunnel: ${e.message}")
|
||||
flutterError(result, e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(wgQuickConfig: String, result: Result) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (!havePermission) {
|
||||
checkPermission()
|
||||
throw Exception("Permissions are not given")
|
||||
}
|
||||
updateStage("prepare")
|
||||
val inputStream = ByteArrayInputStream(wgQuickConfig.toByteArray())
|
||||
config = org.amnezia.awg.config.Config.parse(inputStream)
|
||||
updateStage("connecting")
|
||||
futureBackend.await().setState(
|
||||
tunnel(tunnelName) { state ->
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Log.i(TAG, "onStateChange - $state")
|
||||
updateStageFromState(state)
|
||||
}
|
||||
}, Tunnel.State.UP, config
|
||||
)
|
||||
Log.i(TAG, "Connect - success!")
|
||||
flutterSuccess(result, "")
|
||||
} catch (e: BackendException) {
|
||||
Log.e(TAG, "Connect - BackendException - ERROR - ${e.reason}", e)
|
||||
flutterError(result, e.reason.toString())
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Connect - Can't connect to tunnel: $e", e)
|
||||
flutterError(result, e.message.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTunnel(localizedDescription: String, result: Result) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (Tunnel.isNameInvalid(localizedDescription)) {
|
||||
flutterError(result, "Invalid Name")
|
||||
return@launch
|
||||
}
|
||||
tunnelName = localizedDescription
|
||||
checkPermission()
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission() {
|
||||
val intent = android.net.VpnService.prepare(this.activity)
|
||||
if (intent != null) {
|
||||
havePermission = false
|
||||
this.activity?.startActivityForResult(intent, PERMISSIONS_REQUEST_CODE)
|
||||
} else {
|
||||
havePermission = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
events.setStreamHandler(null)
|
||||
isVpnChecked = false
|
||||
}
|
||||
|
||||
private fun tunnel(name: String, callback: StateChangeCallback? = null): WireGuardTunnel {
|
||||
if (tunnel == null) {
|
||||
tunnel = WireGuardTunnel(name, callback)
|
||||
}
|
||||
return tunnel as WireGuardTunnel
|
||||
}
|
||||
}
|
||||
|
||||
typealias StateChangeCallback = (Tunnel.State) -> Unit
|
||||
|
||||
class WireGuardTunnel(
|
||||
private val name: String, private val onStateChanged: StateChangeCallback? = null
|
||||
) : Tunnel {
|
||||
|
||||
override fun getName() = name
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
onStateChanged?.invoke(newState)
|
||||
}
|
||||
|
||||
override fun isIpv4ResolutionPreferred(): Boolean? = true
|
||||
|
||||
override fun isMetered(): Boolean? = false
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
|
||||
import '../wireguard_flutter_platform_interface.dart';
|
||||
|
||||
class WireGuardFlutterLinux extends WireGuardFlutterInterface {
|
||||
String? name;
|
||||
File? configFile;
|
||||
|
||||
VpnStage _stage = VpnStage.noConnection;
|
||||
final _stageController = StreamController<VpnStage>.broadcast();
|
||||
void _setStage(VpnStage stage) {
|
||||
_stage = stage;
|
||||
_stageController.add(stage);
|
||||
}
|
||||
|
||||
final shell = Shell(runInShell: true, verbose: kDebugMode);
|
||||
|
||||
@override
|
||||
Future<void> initialize({required String interfaceName}) async {
|
||||
name = interfaceName.replaceAll(' ', '_');
|
||||
await refreshStage();
|
||||
}
|
||||
|
||||
Future<String> get filePath async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
return '${tempDir.path}${Platform.pathSeparator}$name.conf';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVpn({
|
||||
required String serverAddress,
|
||||
required String wgQuickConfig,
|
||||
required String providerBundleIdentifier,
|
||||
}) async {
|
||||
final isAlreadyConnected = await isConnected();
|
||||
if (!isAlreadyConnected) {
|
||||
_setStage(VpnStage.preparing);
|
||||
} else {
|
||||
debugPrint('Already connected');
|
||||
}
|
||||
|
||||
try {
|
||||
configFile = await File(await filePath).create();
|
||||
await configFile!.writeAsString(wgQuickConfig);
|
||||
} on PathAccessException {
|
||||
debugPrint('Denied to write file. Trying to start interface');
|
||||
if (isAlreadyConnected) {
|
||||
return _setStage(VpnStage.connected);
|
||||
}
|
||||
|
||||
try {
|
||||
await shell.run('sudo wg-quick up $name');
|
||||
} catch (_) {
|
||||
} finally {
|
||||
_setStage(VpnStage.denied);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAlreadyConnected) {
|
||||
_setStage(VpnStage.connecting);
|
||||
await shell.run('sudo wg-quick up ${configFile?.path ?? await filePath}');
|
||||
_setStage(VpnStage.connected);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopVpn() async {
|
||||
assert(
|
||||
(await isConnected()),
|
||||
'Bad state: vpn has not been started. Call startVpn',
|
||||
);
|
||||
_setStage(VpnStage.disconnecting);
|
||||
try {
|
||||
await shell
|
||||
.run('sudo wg-quick down ${configFile?.path ?? (await filePath)}');
|
||||
} catch (e) {
|
||||
await refreshStage();
|
||||
rethrow;
|
||||
}
|
||||
await refreshStage();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VpnStage> stage() async => _stage;
|
||||
|
||||
@override
|
||||
Stream<VpnStage> get vpnStageSnapshot => _stageController.stream;
|
||||
|
||||
@override
|
||||
Future<void> refreshStage() async {
|
||||
if (await isConnected()) {
|
||||
_setStage(VpnStage.connected);
|
||||
} else if (name == null) {
|
||||
_setStage(VpnStage.waitingConnection);
|
||||
} else if (configFile == null) {
|
||||
_setStage(VpnStage.noConnection);
|
||||
} else {
|
||||
_setStage(VpnStage.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isConnected() async {
|
||||
assert(
|
||||
name != null,
|
||||
'Bad state: not initialized. Call "initialize" before calling this command',
|
||||
);
|
||||
final processResultList = await shell.run('sudo wg');
|
||||
final process = processResultList.first;
|
||||
return process.outLines.any((line) => line.trim() == 'interface: $name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:wireguard_flutter/linux/wireguard_flutter_linux.dart';
|
||||
import 'package:wireguard_flutter/wireguard_flutter_method_channel.dart';
|
||||
|
||||
import 'wireguard_flutter_platform_interface.dart';
|
||||
|
||||
export 'wireguard_flutter_platform_interface.dart' show VpnStage;
|
||||
|
||||
class WireGuardFlutter extends WireGuardFlutterInterface {
|
||||
static WireGuardFlutterInterface? __instance;
|
||||
static WireGuardFlutterInterface get _instance => __instance!;
|
||||
static WireGuardFlutterInterface get instance {
|
||||
registerWith();
|
||||
return _instance;
|
||||
}
|
||||
|
||||
static void registerWith() {
|
||||
if (__instance == null) {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError('The web platform is not supported');
|
||||
} else if (Platform.isLinux) {
|
||||
__instance = WireGuardFlutterLinux();
|
||||
} else {
|
||||
__instance = WireGuardFlutterMethodChannel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WireGuardFlutter._();
|
||||
|
||||
@override
|
||||
Stream<VpnStage> get vpnStageSnapshot => _instance.vpnStageSnapshot;
|
||||
|
||||
@override
|
||||
Future<void> initialize({required String interfaceName}) {
|
||||
return _instance.initialize(interfaceName: interfaceName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVpn({
|
||||
required String serverAddress,
|
||||
required String wgQuickConfig,
|
||||
required String providerBundleIdentifier,
|
||||
}) async {
|
||||
return _instance.startVpn(
|
||||
serverAddress: serverAddress,
|
||||
wgQuickConfig: wgQuickConfig,
|
||||
providerBundleIdentifier: providerBundleIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopVpn() => _instance.stopVpn();
|
||||
|
||||
@override
|
||||
Future<void> refreshStage() => _instance.refreshStage();
|
||||
|
||||
@override
|
||||
Future<VpnStage> stage() => _instance.stage();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'wireguard_flutter_platform_interface.dart';
|
||||
|
||||
class WireGuardFlutterMethodChannel extends WireGuardFlutterInterface {
|
||||
static const _methodChannelVpnControl =
|
||||
"billion.group.wireguard_flutter/wgcontrol";
|
||||
static const _methodChannel = MethodChannel(_methodChannelVpnControl);
|
||||
static const _eventChannelVpnStage =
|
||||
'billion.group.wireguard_flutter/wgstage';
|
||||
static const _eventChannel = EventChannel(_eventChannelVpnStage);
|
||||
|
||||
@override
|
||||
Stream<VpnStage> get vpnStageSnapshot =>
|
||||
_eventChannel.receiveBroadcastStream().map(
|
||||
(event) => event == VpnStage.denied.code
|
||||
? VpnStage.disconnected
|
||||
: VpnStage.values.firstWhere(
|
||||
(stage) => stage.code == event,
|
||||
orElse: () => VpnStage.noConnection,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> initialize({required String interfaceName}) {
|
||||
return _methodChannel.invokeMethod("initialize", {
|
||||
"localizedDescription": interfaceName,
|
||||
"win32ServiceName": interfaceName,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVpn({
|
||||
required String serverAddress,
|
||||
required String wgQuickConfig,
|
||||
required String providerBundleIdentifier,
|
||||
}) async {
|
||||
return _methodChannel.invokeMethod("start", {
|
||||
"serverAddress": serverAddress,
|
||||
"wgQuickConfig": wgQuickConfig,
|
||||
"providerBundleIdentifier": providerBundleIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopVpn() => _methodChannel.invokeMethod('stop');
|
||||
|
||||
@override
|
||||
Future<void> refreshStage() => _methodChannel.invokeMethod("refresh");
|
||||
|
||||
@override
|
||||
Future<VpnStage> stage() => _methodChannel.invokeMethod("stage").then(
|
||||
(value) => value != null
|
||||
? VpnStage.values.firstWhere(
|
||||
(stage) => stage.code == value.toString(),
|
||||
orElse: () => VpnStage.disconnected,
|
||||
)
|
||||
: VpnStage.disconnected,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
abstract class WireGuardFlutterInterface {
|
||||
Stream<VpnStage> get vpnStageSnapshot;
|
||||
|
||||
Future<void> initialize({required String interfaceName});
|
||||
|
||||
Future<void> startVpn({
|
||||
required String serverAddress,
|
||||
required String wgQuickConfig,
|
||||
required String providerBundleIdentifier,
|
||||
});
|
||||
|
||||
Future<void> stopVpn();
|
||||
|
||||
Future<void> refreshStage();
|
||||
Future<VpnStage> stage();
|
||||
Future<bool> isConnected() =>
|
||||
stage().then((stage) => stage == VpnStage.connected);
|
||||
}
|
||||
|
||||
enum VpnStage {
|
||||
connected('connected'),
|
||||
connecting('connecting'),
|
||||
disconnecting('disconnecting'),
|
||||
disconnected('disconnected'),
|
||||
waitingConnection('wait_connection'),
|
||||
authenticating('authenticating'),
|
||||
reconnect('reconnect'),
|
||||
noConnection('no_connection'),
|
||||
preparing('prepare'),
|
||||
denied('denied'),
|
||||
exiting('exiting');
|
||||
|
||||
final String code;
|
||||
|
||||
const VpnStage(this.code);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
name: wireguard_flutter
|
||||
description: Wireguard Flutter SDK
|
||||
version: 0.1.3
|
||||
homepage: https://flutterflux.com
|
||||
repository: https://github.com/Caqil/wireguard_flutter
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: '>=3.13.7'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
path_provider: ^2.1.2
|
||||
plugin_platform_interface: ^2.0.2
|
||||
process_run: ^0.14.0+1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
package: billion.group.wireguard_flutter
|
||||
pluginClass: WireguardFlutterPlugin
|
||||
@@ -0,0 +1,729 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cryptography:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cryptography
|
||||
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.1"
|
||||
flutter_secure_storage_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_darwin
|
||||
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
google_mobile_ads:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_mobile_ads
|
||||
sha256: d2ef5ec1e1f31137fc241bdeab3037c31062d387dd221fd884fb1160444c788b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
process_run:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process_run
|
||||
sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.2"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.30"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
webview_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.16.9"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.15.1"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.25.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
wireguard_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "local_packages/wireguard_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
@@ -0,0 +1,85 @@
|
||||
name: paladinvpn
|
||||
description: Paladin VPN — Minimalist high-performance WireGuard tunnel
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.1+2
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.5
|
||||
google_fonts: ^6.1.0
|
||||
provider: ^6.1.1
|
||||
google_mobile_ads: ^4.0.0
|
||||
wireguard_flutter:
|
||||
path: local_packages/wireguard_flutter
|
||||
http: ^1.6.0
|
||||
shared_preferences: ^2.5.5
|
||||
cryptography: ^2.9.0
|
||||
url_launcher: ^6.3.2
|
||||
flutter_secure_storage: ^10.3.1
|
||||
uuid: ^4.5.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||