react-native-ovpn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +20 -0
  2. package/Openvpn.podspec +34 -0
  3. package/README.md +80 -0
  4. package/android/build.gradle +98 -0
  5. package/android/libs/README.md +46 -0
  6. package/android/libs/ics-openvpn.aar +0 -0
  7. package/android/src/main/AndroidManifest.xml +54 -0
  8. package/android/src/main/java/com/openvpn/NotificationHelper.kt +59 -0
  9. package/android/src/main/java/com/openvpn/OpenvpnEventBus.kt +52 -0
  10. package/android/src/main/java/com/openvpn/OpenvpnException.kt +6 -0
  11. package/android/src/main/java/com/openvpn/OpenvpnModule.kt +140 -0
  12. package/android/src/main/java/com/openvpn/OpenvpnPackage.kt +31 -0
  13. package/android/src/main/java/com/openvpn/OpenvpnService.kt +248 -0
  14. package/android/src/main/java/com/openvpn/PermissionLauncher.kt +39 -0
  15. package/android/src/main/java/com/openvpn/ProfileBuilder.kt +68 -0
  16. package/android/src/main/res/drawable/ic_vpn_default.xml +10 -0
  17. package/android/src/main/res/values/strings.xml +6 -0
  18. package/android/src/test/java/com/openvpn/NotificationHelperTest.kt +49 -0
  19. package/android/src/test/java/com/openvpn/ProfileBuilderTest.kt +83 -0
  20. package/app.plugin.js +3 -0
  21. package/ios/Openvpn-Bridging-Header.h +8 -0
  22. package/ios/Openvpn.h +5 -0
  23. package/ios/Openvpn.mm +123 -0
  24. package/ios/OpenvpnAppGroup.swift +59 -0
  25. package/ios/OpenvpnConstants.swift +46 -0
  26. package/ios/OpenvpnEventBridge.swift +58 -0
  27. package/ios/OpenvpnManager.swift +219 -0
  28. package/ios/PacketTunnelProvider/Info.plist +31 -0
  29. package/ios/PacketTunnelProvider/PacketTunnelProvider.swift +199 -0
  30. package/ios/PacketTunnelProvider/README.md +106 -0
  31. package/lib/module/NativeOpenvpn.js +5 -0
  32. package/lib/module/NativeOpenvpn.js.map +1 -0
  33. package/lib/module/OpenVPNClient.js +185 -0
  34. package/lib/module/OpenVPNClient.js.map +1 -0
  35. package/lib/module/errors.js +13 -0
  36. package/lib/module/errors.js.map +1 -0
  37. package/lib/module/index.js +5 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/reconnect.js +51 -0
  41. package/lib/module/reconnect.js.map +1 -0
  42. package/lib/module/types.js +2 -0
  43. package/lib/module/types.js.map +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/plugin/src/android/index.d.ts +5 -0
  46. package/lib/typescript/plugin/src/android/index.d.ts.map +1 -0
  47. package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts +5 -0
  48. package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts.map +1 -0
  49. package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts +5 -0
  50. package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts.map +1 -0
  51. package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts +5 -0
  52. package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts.map +1 -0
  53. package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts +5 -0
  54. package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts.map +1 -0
  55. package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts +5 -0
  56. package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts.map +1 -0
  57. package/lib/typescript/plugin/src/android/withAndroidService.d.ts +5 -0
  58. package/lib/typescript/plugin/src/android/withAndroidService.d.ts.map +1 -0
  59. package/lib/typescript/plugin/src/index.d.ts +6 -0
  60. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  61. package/lib/typescript/plugin/src/ios/index.d.ts +5 -0
  62. package/lib/typescript/plugin/src/ios/index.d.ts.map +1 -0
  63. package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts +5 -0
  64. package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts.map +1 -0
  65. package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts +5 -0
  66. package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts.map +1 -0
  67. package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts +5 -0
  68. package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts.map +1 -0
  69. package/lib/typescript/plugin/src/types.d.ts +14 -0
  70. package/lib/typescript/plugin/src/types.d.ts.map +1 -0
  71. package/lib/typescript/src/NativeOpenvpn.d.ts +41 -0
  72. package/lib/typescript/src/NativeOpenvpn.d.ts.map +1 -0
  73. package/lib/typescript/src/OpenVPNClient.d.ts +37 -0
  74. package/lib/typescript/src/OpenVPNClient.d.ts.map +1 -0
  75. package/lib/typescript/src/errors.d.ts +9 -0
  76. package/lib/typescript/src/errors.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +5 -0
  78. package/lib/typescript/src/index.d.ts.map +1 -0
  79. package/lib/typescript/src/reconnect.d.ts +23 -0
  80. package/lib/typescript/src/reconnect.d.ts.map +1 -0
  81. package/lib/typescript/src/types.d.ts +41 -0
  82. package/lib/typescript/src/types.d.ts.map +1 -0
  83. package/package.json +193 -0
  84. package/plugin/build/android/index.d.ts +4 -0
  85. package/plugin/build/android/index.js +24 -0
  86. package/plugin/build/android/withAndroidAarCheck.d.ts +4 -0
  87. package/plugin/build/android/withAndroidAarCheck.js +60 -0
  88. package/plugin/build/android/withAndroidLegacyPackaging.d.ts +4 -0
  89. package/plugin/build/android/withAndroidLegacyPackaging.js +18 -0
  90. package/plugin/build/android/withAndroidMinSdk.d.ts +4 -0
  91. package/plugin/build/android/withAndroidMinSdk.js +13 -0
  92. package/plugin/build/android/withAndroidNotificationIcon.d.ts +4 -0
  93. package/plugin/build/android/withAndroidNotificationIcon.js +64 -0
  94. package/plugin/build/android/withAndroidPermissions.d.ts +4 -0
  95. package/plugin/build/android/withAndroidPermissions.js +30 -0
  96. package/plugin/build/android/withAndroidService.d.ts +4 -0
  97. package/plugin/build/android/withAndroidService.js +40 -0
  98. package/plugin/build/index.d.ts +5 -0
  99. package/plugin/build/index.js +18 -0
  100. package/plugin/build/ios/index.d.ts +4 -0
  101. package/plugin/build/ios/index.js +15 -0
  102. package/plugin/build/ios/withIosDeploymentTarget.d.ts +4 -0
  103. package/plugin/build/ios/withIosDeploymentTarget.js +28 -0
  104. package/plugin/build/ios/withIosEntitlements.d.ts +4 -0
  105. package/plugin/build/ios/withIosEntitlements.js +15 -0
  106. package/plugin/build/ios/withIosInfoPlist.d.ts +4 -0
  107. package/plugin/build/ios/withIosInfoPlist.js +14 -0
  108. package/plugin/build/types.d.ts +13 -0
  109. package/plugin/build/types.js +2 -0
  110. package/src/NativeOpenvpn.ts +46 -0
  111. package/src/OpenVPNClient.ts +239 -0
  112. package/src/errors.ts +29 -0
  113. package/src/index.ts +12 -0
  114. package/src/reconnect.ts +68 -0
  115. package/src/types.ts +53 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rasel
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "Openvpn"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "13.0" }
14
+ s.source = { :git => "https://github.com/Raselj71/react-native-ovpn.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/*.h"
18
+
19
+ # The PacketTunnelProvider sources are templates copied into the consumer's
20
+ # own Network Extension target. They must NOT be compiled into our pod (host
21
+ # app code), so exclude them here.
22
+ s.exclude_files = "ios/PacketTunnelProvider/**/*"
23
+
24
+ # OpenVPNAdapter — AGPLv3, the canonical iOS OpenVPN binding.
25
+ s.dependency 'OpenVPNAdapter', '~> 0.8.0'
26
+
27
+ s.pod_target_xcconfig = {
28
+ "SWIFT_OBJC_BRIDGING_HEADER" => "$(PODS_TARGET_SRCROOT)/ios/Openvpn-Bridging-Header.h",
29
+ "DEFINES_MODULE" => "YES",
30
+ "SWIFT_VERSION" => "5.0"
31
+ }
32
+
33
+ install_modules_dependencies(s)
34
+ end
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # react-native-ovpn
2
+
3
+ [![npm](https://img.shields.io/npm/v/react-native-ovpn.svg)](https://www.npmjs.com/package/react-native-ovpn)
4
+ [![license](https://img.shields.io/npm/l/react-native-ovpn.svg)](LICENSE)
5
+
6
+ OpenVPN client for React Native — Android, iOS, and Expo. New Architecture (TurboModule), bounded auto-reconnect, kill-switch, custom DNS.
7
+
8
+ ```ts
9
+ import { OpenVPNClient } from 'react-native-ovpn';
10
+
11
+ const client = new OpenVPNClient();
12
+ client.on('state', (s) => console.log(s)); // 'connecting' | 'connected' | ...
13
+ client.on('stats', ({ bytesIn, bytesOut }) => {});
14
+
15
+ await client.requestPermission();
16
+ await client.connect({ config: ovpnString, username: 'alice', password: 's3cret' });
17
+ await client.disconnect();
18
+ ```
19
+
20
+ ## Documentation
21
+
22
+ - **[Getting started](docs/getting-started.md)** — install, Android setup, iOS setup, Expo setup
23
+ - **[API reference](docs/api.md)** — full method/event/type reference
24
+ - **[OpenVPN config support](docs/ovpn-support.md)** — which `.ovpn` directives work
25
+ - **[Recipes](docs/examples.md)** — kill-switch, custom DNS, reconnect, 2FA
26
+ - **[Troubleshooting](docs/troubleshooting.md)** — common errors and fixes
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install react-native-ovpn
32
+ # or
33
+ yarn add react-native-ovpn
34
+ # or
35
+ pnpm add react-native-ovpn
36
+ ```
37
+
38
+ **Expo:** add to `app.config.js`:
39
+
40
+ ```js
41
+ plugins: [
42
+ ['react-native-ovpn', {
43
+ iosAppGroup: 'group.com.example.myapp.openvpn',
44
+ iosExtensionBundleIdentifier: 'com.example.myapp.OpenVPNTunnel',
45
+ }],
46
+ ];
47
+ ```
48
+
49
+ Then `npx expo prebuild --clean`.
50
+
51
+ **Bare React Native:** see [Getting started](docs/getting-started.md).
52
+
53
+ ## Feature matrix
54
+
55
+ | Feature | Android | iOS |
56
+ | --- | --- | --- |
57
+ | Connect / disconnect | ✅ | ✅ |
58
+ | State + stats + log events | ✅ | ✅ |
59
+ | Auto-reconnect (bounded backoff) | ✅ | ✅ |
60
+ | Username + password auth | ✅ | ✅ |
61
+ | Certificate-only auth | ✅ | ✅ |
62
+ | TLS-auth / tls-crypt / tls-crypt-v2 | ✅ | ✅ |
63
+ | Legacy cipher fallback (AES-128-CBC) | ✅ (auto-injected) | ✅ |
64
+ | Custom DNS override | ✅ | ✅ |
65
+ | Kill-switch | ✅ | ❌ |
66
+ | Foreground notification | ✅ | n/a |
67
+ | Per-app routing | ❌ | ❌ |
68
+ | Expo config plugin | ✅ | ✅ |
69
+
70
+ See [OpenVPN config support](docs/ovpn-support.md) for the full directive-level matrix.
71
+
72
+ ## License
73
+
74
+ Wrapper code: MIT.
75
+
76
+ **Important** — Android embeds [ics-openvpn](https://github.com/schwabe/ics-openvpn) (**GPLv2**), iOS embeds [OpenVPNAdapter](https://github.com/ss-abramchuk/OpenVPNAdapter) (**AGPLv3**). Apps that ship `react-native-ovpn` inherit those copyleft obligations. Fine for personal, internal, or open-source apps; **not compatible** with closed-source commercial distribution without separate commercial agreements with the upstream projects.
77
+
78
+ ## Contributing
79
+
80
+ [Development workflow](CONTRIBUTING.md) · [Code of conduct](CODE_OF_CONDUCT.md)
@@ -0,0 +1,98 @@
1
+ buildscript {
2
+ ext.Openvpn = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return Openvpn[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.openvpn"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+
64
+ testOptions {
65
+ unitTests {
66
+ includeAndroidResources = true
67
+ returnDefaultValues = true
68
+ }
69
+ }
70
+
71
+ packagingOptions {
72
+ resources {
73
+ excludes += [
74
+ "META-INF/AL2.0",
75
+ "META-INF/LGPL2.1",
76
+ "**/*.po",
77
+ ]
78
+ }
79
+ }
80
+ }
81
+
82
+ repositories {
83
+ google()
84
+ mavenCentral()
85
+ }
86
+
87
+ dependencies {
88
+ implementation "com.facebook.react:react-android"
89
+ // ics-openvpn engine, committed to the repo at android/libs/ — see
90
+ // android/libs/README.md for how it was produced.
91
+ implementation files("$projectDir/libs/ics-openvpn.aar")
92
+ implementation "androidx.core:core-ktx:1.13.1"
93
+
94
+ testImplementation "junit:junit:4.13.2"
95
+ testImplementation "org.robolectric:robolectric:4.13"
96
+ testImplementation "androidx.test:core:1.5.0"
97
+ testImplementation "androidx.test.ext:junit:1.1.5"
98
+ }
@@ -0,0 +1,46 @@
1
+ # `android/libs/` — vendored AAR location
2
+
3
+ `ics-openvpn.aar` lives here and **is committed to the repository**. Consumers and CI never need to rebuild it. Rebuild only when bumping the ics-openvpn submodule pointer.
4
+
5
+ ```
6
+ android/libs/ics-openvpn.aar (~5–10 MB binary, tracked in git)
7
+ ```
8
+
9
+ ## How it was produced (rebuilding when needed)
10
+
11
+ ics-openvpn's upstream `main` Gradle module is an **application**, not a library, so we cannot consume it via a normal `implementation project(...)` dependency or a Maven coordinate. Instead, we build it locally once into an AAR and commit the binary.
12
+
13
+ Run the build script from the repo root:
14
+
15
+ ```powershell
16
+ pwsh ./scripts/build-ics-openvpn-aar.ps1
17
+ ```
18
+
19
+ That script:
20
+
21
+ 1. Patches `third_party/ics-openvpn/main/build.gradle.kts` in-place to use the `com.android.library` plugin (so the build emits an AAR).
22
+ 2. Runs `./gradlew :main:assembleSkeletonOvpn23Release` inside the submodule (skeleton = no UI; ovpn23 = both OpenVPN 2.x and 3.x).
23
+ 3. Copies the resulting AAR to `android/libs/ics-openvpn.aar`.
24
+ 4. Reverts the gradle patch so the submodule stays clean.
25
+
26
+ After running the script, **commit the AAR** so other clones and CI don't need NDK:
27
+
28
+ ```bash
29
+ git add android/libs/ics-openvpn.aar
30
+ git commit -m "build(android): vendor ics-openvpn AAR (v0.7.59 skeleton-ovpn23)"
31
+ ```
32
+
33
+ ## Prerequisites for running the script
34
+
35
+ - Java 17+ (Temurin or Android Studio JBR)
36
+ - Android SDK with build-tools and platform-35
37
+ - Android NDK 27.x or 28.x
38
+ - CMake 3.22+ (Android SDK CMake component)
39
+ - SWIG (`choco install swig` on Windows; `brew install swig` on macOS)
40
+ - **Nested submodules initialized:** `git -C third_party/ics-openvpn submodule update --init --recursive` (pulls in asio, lz4, mbedtls, openssl, openvpn, openvpn3)
41
+
42
+ ## When to rebuild
43
+
44
+ - After bumping the submodule pointer (security fixes, new ics-openvpn release)
45
+ - After changing the chosen flavor (e.g., `ovpn2` instead of `ovpn23`)
46
+ - Otherwise: never — the AAR is a stable artifact
Binary file
@@ -0,0 +1,54 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:tools="http://schemas.android.com/tools">
4
+
5
+ <uses-permission android:name="android.permission.INTERNET" />
6
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
7
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
8
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
9
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
10
+ <!-- ics-openvpn schedules a PERSISTED keepalive JobService that survives
11
+ reboots; the OS requires this permission on apps that do that. -->
12
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
13
+
14
+ <!--
15
+ The bundled ics-openvpn.aar declares <application android:name=... label=... theme=...>
16
+ inherited from its original "application" build. Tell the manifest merger to drop
17
+ those attributes at this layer so they don't conflict with the consumer's app manifest.
18
+ -->
19
+ <application
20
+ tools:remove="android:name,android:label,android:theme,android:icon,android:allowBackup">
21
+ <service
22
+ android:name="com.openvpn.OpenvpnService"
23
+ android:permission="android.permission.BIND_VPN_SERVICE"
24
+ android:foregroundServiceType="specialUse"
25
+ android:exported="false">
26
+ <property
27
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
28
+ android:value="vpn" />
29
+ <intent-filter>
30
+ <action android:name="android.net.VpnService" />
31
+ </intent-filter>
32
+ </service>
33
+
34
+ <!--
35
+ ics-openvpn's OpenVPNService.startOpenVPN schedules this JobService
36
+ directly via JobScheduler. The original AAR manifest declared it but
37
+ we stripped the AAR manifest to avoid <application>-attribute conflicts.
38
+ Re-declare it here so Android can find the component.
39
+ -->
40
+ <service
41
+ android:name="de.blinkt.openvpn.core.keepVPNAlive"
42
+ android:exported="false"
43
+ android:permission="android.permission.BIND_JOB_SERVICE" />
44
+
45
+ <!--
46
+ Status-broadcast service used by ics-openvpn internally for IPC between
47
+ the foreground OpenVPN service and its API consumers. Declared here for
48
+ the same reason as keepVPNAlive.
49
+ -->
50
+ <service
51
+ android:name="de.blinkt.openvpn.core.OpenVPNStatusService"
52
+ android:exported="false" />
53
+ </application>
54
+ </manifest>
@@ -0,0 +1,59 @@
1
+ package com.openvpn
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.content.Context
7
+ import android.os.Build
8
+ import androidx.core.app.NotificationCompat
9
+
10
+ data class NotificationOverrides(
11
+ val title: String? = null,
12
+ val text: String? = null,
13
+ val smallIcon: String? = null,
14
+ val channelId: String? = null,
15
+ )
16
+
17
+ object NotificationHelper {
18
+
19
+ private const val DEFAULT_CHANNEL_ID = "openvpn"
20
+
21
+ fun build(context: Context, overrides: NotificationOverrides): Notification {
22
+ val channelId = overrides.channelId ?: DEFAULT_CHANNEL_ID
23
+ ensureChannel(
24
+ context,
25
+ channelId,
26
+ context.getString(R.string.openvpn_notification_channel_name),
27
+ )
28
+
29
+ val title = overrides.title ?: context.getString(R.string.openvpn_notification_title)
30
+ val text = overrides.text ?: context.getString(R.string.openvpn_notification_text)
31
+ val iconResId = resolveIcon(context, overrides.smallIcon)
32
+
33
+ return NotificationCompat.Builder(context, channelId)
34
+ .setContentTitle(title)
35
+ .setContentText(text)
36
+ .setSmallIcon(iconResId)
37
+ .setOngoing(true)
38
+ .setPriority(NotificationCompat.PRIORITY_LOW)
39
+ .build()
40
+ }
41
+
42
+ fun ensureChannel(context: Context, channelId: String, channelName: String) {
43
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
44
+ val mgr = context.getSystemService(NotificationManager::class.java) ?: return
45
+ if (mgr.getNotificationChannel(channelId) != null) return
46
+ val channel = NotificationChannel(
47
+ channelId,
48
+ channelName,
49
+ NotificationManager.IMPORTANCE_LOW,
50
+ )
51
+ mgr.createNotificationChannel(channel)
52
+ }
53
+
54
+ private fun resolveIcon(context: Context, name: String?): Int {
55
+ if (name.isNullOrBlank()) return R.drawable.ic_vpn_default
56
+ val res = context.resources.getIdentifier(name, "drawable", context.packageName)
57
+ return if (res != 0) res else R.drawable.ic_vpn_default
58
+ }
59
+ }
@@ -0,0 +1,52 @@
1
+ package com.openvpn
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.modules.core.DeviceEventManagerModule
6
+
7
+ object OpenvpnEventBus {
8
+
9
+ @Volatile
10
+ private var reactContext: ReactApplicationContext? = null
11
+
12
+ fun attach(context: ReactApplicationContext) {
13
+ reactContext = context
14
+ }
15
+
16
+ fun detach() {
17
+ reactContext = null
18
+ }
19
+
20
+ fun emitState(state: String) {
21
+ emit("OpenVpn:state", state)
22
+ }
23
+
24
+ fun emitStats(bytesIn: Long, bytesOut: Long, durationMs: Long) {
25
+ val payload = Arguments.createMap().apply {
26
+ putDouble("bytesIn", bytesIn.toDouble())
27
+ putDouble("bytesOut", bytesOut.toDouble())
28
+ putDouble("durationMs", durationMs.toDouble())
29
+ }
30
+ emit("OpenVpn:stats", payload)
31
+ }
32
+
33
+ fun emitLog(line: String) {
34
+ emit("OpenVpn:log", line)
35
+ }
36
+
37
+ fun emitError(code: String, nativeMessage: String?) {
38
+ val payload = Arguments.createMap().apply {
39
+ putString("code", code)
40
+ putString("nativeMessage", nativeMessage)
41
+ }
42
+ emit("OpenVpn:error", payload)
43
+ }
44
+
45
+ private fun emit(eventName: String, payload: Any?) {
46
+ val ctx = reactContext ?: return
47
+ if (!ctx.hasActiveReactInstance()) return
48
+ ctx
49
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
50
+ .emit(eventName, payload)
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ package com.openvpn
2
+
3
+ class OpenvpnException(
4
+ val code: String,
5
+ val nativeMessage: String,
6
+ ) : RuntimeException("$code: $nativeMessage")
@@ -0,0 +1,140 @@
1
+ package com.openvpn
2
+
3
+ import android.content.Intent
4
+ import android.os.Build
5
+ import com.facebook.react.bridge.ActivityEventListener
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.LifecycleEventListener
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.bridge.WritableMap
12
+
13
+ class OpenvpnModule(reactContext: ReactApplicationContext) :
14
+ NativeOpenvpnSpec(reactContext), LifecycleEventListener, ActivityEventListener {
15
+
16
+ private val context: ReactApplicationContext = reactContext
17
+
18
+ init {
19
+ OpenvpnEventBus.attach(reactContext)
20
+ reactContext.addLifecycleEventListener(this)
21
+ reactContext.addActivityEventListener(this)
22
+ }
23
+
24
+ override fun getName(): String = NAME
25
+
26
+ override fun requestPermission(promise: Promise) {
27
+ PermissionLauncher.request(context, promise)
28
+ }
29
+
30
+ override fun connect(params: ReadableMap, promise: Promise) {
31
+ try {
32
+ // Reject per-app routing (deferred from Plan 2)
33
+ val allowed = if (params.hasKey("allowedApps")) params.getArray("allowedApps") else null
34
+ val disallowed = if (params.hasKey("disallowedApps")) params.getArray("disallowedApps") else null
35
+ if ((allowed?.size() ?: 0) > 0 || (disallowed?.size() ?: 0) > 0) {
36
+ promise.reject(
37
+ "INVALID_CONFIG",
38
+ "per-app routing (allowedApps/disallowedApps) not yet implemented on Android",
39
+ )
40
+ return
41
+ }
42
+
43
+ val ovpn = params.getString("config")
44
+ ?: throw OpenvpnException("INVALID_CONFIG", "config missing")
45
+ val username = params.getString("username") ?: ""
46
+ val password = params.getString("password") ?: ""
47
+ val killSwitch = if (params.hasKey("killSwitch")) params.getBoolean("killSwitch") else false
48
+ val dns = if (params.hasKey("dns")) {
49
+ params.getArray("dns")?.let { array ->
50
+ (0 until array.size()).mapNotNull { array.getString(it) }
51
+ } ?: emptyList()
52
+ } else emptyList()
53
+ val notificationMap = if (params.hasKey("notification")) params.getMap("notification") else null
54
+
55
+ val profile = ProfileBuilder.build(ovpn, username, password, dns, killSwitch)
56
+
57
+ val intent = Intent(context, OpenvpnService::class.java).apply {
58
+ action = OpenvpnService.ACTION_START
59
+ putExtra(OpenvpnService.EXTRA_PROFILE, profile)
60
+ notificationMap?.let {
61
+ if (it.hasKey("title")) putExtra(OpenvpnService.EXTRA_NOTIF_TITLE, it.getString("title"))
62
+ if (it.hasKey("text")) putExtra(OpenvpnService.EXTRA_NOTIF_TEXT, it.getString("text"))
63
+ if (it.hasKey("smallIcon")) putExtra(OpenvpnService.EXTRA_NOTIF_ICON, it.getString("smallIcon"))
64
+ if (it.hasKey("channelId")) putExtra(OpenvpnService.EXTRA_NOTIF_CHANNEL, it.getString("channelId"))
65
+ }
66
+ }
67
+
68
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69
+ context.startForegroundService(intent)
70
+ } else {
71
+ context.startService(intent)
72
+ }
73
+ // The JS-side connect() Promise resolves on the 'connected' state event,
74
+ // so we resolve this native promise as soon as the start request is
75
+ // accepted by the system.
76
+ promise.resolve(null)
77
+ } catch (e: OpenvpnException) {
78
+ promise.reject(e.code, e.nativeMessage)
79
+ } catch (e: Exception) {
80
+ promise.reject("NATIVE_ERROR", e.message ?: "unknown error")
81
+ }
82
+ }
83
+
84
+ override fun disconnect(promise: Promise) {
85
+ try {
86
+ val intent = Intent(context, OpenvpnService::class.java).apply {
87
+ action = OpenvpnService.ACTION_STOP
88
+ }
89
+ context.startService(intent)
90
+ promise.resolve(null)
91
+ } catch (e: Exception) {
92
+ promise.reject("NATIVE_ERROR", e.message ?: "unknown error")
93
+ }
94
+ }
95
+
96
+ override fun getStatus(promise: Promise) {
97
+ val map: WritableMap = Arguments.createMap()
98
+ // State is delivered via the 'OpenVpn:state' event channel.
99
+ // This method returns a minimal snapshot; consumers should prefer events.
100
+ map.putString("state", "idle")
101
+ promise.resolve(map)
102
+ }
103
+
104
+ override fun getStats(promise: Promise) {
105
+ val map: WritableMap = Arguments.createMap()
106
+ map.putDouble("bytesIn", 0.0)
107
+ map.putDouble("bytesOut", 0.0)
108
+ map.putDouble("durationMs", 0.0)
109
+ promise.resolve(map)
110
+ }
111
+
112
+ override fun addListener(eventName: String?) {
113
+ // required by RN's NativeEventEmitter contract; no-op
114
+ }
115
+
116
+ override fun removeListeners(count: Double) {
117
+ // required by RN's NativeEventEmitter contract; no-op
118
+ }
119
+
120
+ override fun onHostResume() = Unit
121
+ override fun onHostPause() = Unit
122
+ override fun onHostDestroy() {
123
+ OpenvpnEventBus.detach()
124
+ }
125
+
126
+ override fun onActivityResult(
127
+ activity: android.app.Activity,
128
+ requestCode: Int,
129
+ resultCode: Int,
130
+ data: Intent?,
131
+ ) {
132
+ PermissionLauncher.onActivityResult(requestCode, resultCode, data)
133
+ }
134
+
135
+ override fun onNewIntent(intent: Intent) = Unit
136
+
137
+ companion object {
138
+ const val NAME = NativeOpenvpnSpec.NAME
139
+ }
140
+ }
@@ -0,0 +1,31 @@
1
+ package com.openvpn
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class OpenvpnPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == OpenvpnModule.NAME) {
13
+ OpenvpnModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ OpenvpnModule.NAME to ReactModuleInfo(
22
+ name = OpenvpnModule.NAME,
23
+ className = OpenvpnModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }