@fireota/react-native 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +235 -3
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/dev/fireota/reactnative/FireOTA.kt +33 -0
- package/android/src/main/java/dev/fireota/reactnative/FireOTAModule.kt +105 -0
- package/android/src/main/java/dev/fireota/reactnative/FireOTAPackage.kt +13 -0
- package/android/src/main/java/dev/fireota/reactnative/FireOTARestartActivity.kt +28 -0
- package/android/src/main/java/dev/fireota/reactnative/FireOTAStore.kt +207 -0
- package/fireota-react-native.podspec +19 -0
- package/ios/FireOTA.h +20 -0
- package/ios/FireOTA.m +346 -0
- package/package.json +48 -4
- package/src/index.ts +265 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FireOTA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Sujay-Dynamisch/FireOTA/main/public/logo-mark.png" alt="FireOTA" width="96" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">FireOTA</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Over-the-air updates for React Native — served from <strong>your own Firebase project</strong>.<br />
|
|
9
|
+
Staged rollouts, checksum verification, automatic rollback, audit logs and a CI-ready CLI.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@fireota/react-native"><img alt="@fireota/react-native" src="https://img.shields.io/npm/v/@fireota/react-native?label=%40fireota%2Freact-native" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@fireota/cli"><img alt="@fireota/cli" src="https://img.shields.io/npm/v/@fireota/cli?label=%40fireota%2Fcli" /></a>
|
|
15
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-blue" />
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
Ship JavaScript fixes and features to your React Native app **without a store review**:
|
|
23
|
+
|
|
24
|
+
1. `npx fireota publish` bundles your JS (Hermes bytecode + images) and uploads it to **your** Firebase Storage bucket.
|
|
25
|
+
2. FireOTA records the release — version, runtime, SHA-256, channel, rollout — and writes every action to an audit log.
|
|
26
|
+
3. The SDK in your app checks for a compatible release on launch, downloads it **directly from your bucket**, verifies the checksum, and applies it on the next start.
|
|
27
|
+
4. If the new bundle crashes before starting, the device rolls back to the previous bundle automatically and reports it.
|
|
28
|
+
|
|
29
|
+
You own the artifacts and pay Google directly for storage and bandwidth. FireOTA never proxies downloads.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| **Releases** | Strict state machine (draft → ready → published → paused / rolled back / archived), atomic publish and rollback |
|
|
36
|
+
| **Rollouts** | Deterministic per-device buckets, 0–100 %, per-platform channel targets (Android and iOS independent) |
|
|
37
|
+
| **Safety** | Server-side SHA-256, exact runtime matching, native-version constraints, automatic pause on high failure rates |
|
|
38
|
+
| **Teams** | Organizations, roles (owner, admin, release manager, developer, viewer), Supabase Row Level Security |
|
|
39
|
+
| **Integrations** | Scoped API keys, CI/CD API, HMAC-signed webhooks with retries, notifications, analytics |
|
|
40
|
+
| **Firebase** | Google OAuth (PKCE) per customer project, encrypted refresh tokens, live Storage write verification |
|
|
41
|
+
|
|
42
|
+
## Repository layout
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
src/ Web dashboard + API (TanStack Start, React, Tailwind)
|
|
46
|
+
routes/ Pages and HTTP routes (/api/public/v1/*, /health, /docs)
|
|
47
|
+
lib/backend/ Release engine, OTA resolver, Firebase adapter, RBAC, webhooks
|
|
48
|
+
lib/api/ Server functions used by the dashboard
|
|
49
|
+
components/docs/ Documentation site content (/docs)
|
|
50
|
+
supabase/migrations/ Database schema, RLS policies and transactional functions
|
|
51
|
+
packages/react-native/ @fireota/react-native — the app SDK (Kotlin + Objective-C)
|
|
52
|
+
packages/cli/ @fireota/cli — bundle, upload and publish from a terminal or CI
|
|
53
|
+
tests/ Integration, end-to-end and package-format tests
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start (local)
|
|
57
|
+
|
|
58
|
+
**Prerequisites:** Node.js 20+, a [Supabase](https://supabase.com) project, a Google Cloud OAuth client.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
git clone https://github.com/Sujay-Dynamisch/FireOTA.git
|
|
62
|
+
cd FireOTA
|
|
63
|
+
npm install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 1. Environment
|
|
67
|
+
|
|
68
|
+
Create `.env` in the project root (never commit it):
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
# Supabase -> Project Settings -> API
|
|
72
|
+
SUPABASE_URL="https://<project-ref>.supabase.co"
|
|
73
|
+
SUPABASE_PUBLISHABLE_KEY="sb_publishable_..."
|
|
74
|
+
VITE_SUPABASE_URL="https://<project-ref>.supabase.co"
|
|
75
|
+
VITE_SUPABASE_PUBLISHABLE_KEY="sb_publishable_..."
|
|
76
|
+
SUPABASE_PROJECT_ID="<project-ref>"
|
|
77
|
+
VITE_SUPABASE_PROJECT_ID="<project-ref>"
|
|
78
|
+
|
|
79
|
+
# Server-only secrets
|
|
80
|
+
SUPABASE_SERVICE_ROLE_KEY="..." # Supabase -> API Keys -> Secret key (bypasses RLS)
|
|
81
|
+
FIREOTA_ENCRYPTION_KEY="..." # random; encrypts Google tokens and webhook secrets — never change it
|
|
82
|
+
FIREOTA_CRON_SECRET="..." # bearer token for the maintenance endpoint
|
|
83
|
+
|
|
84
|
+
# Google Cloud -> APIs & Services -> Credentials -> OAuth client (Web application)
|
|
85
|
+
GOOGLE_CLIENT_ID="..."
|
|
86
|
+
GOOGLE_CLIENT_SECRET="..."
|
|
87
|
+
GOOGLE_REDIRECT_URI="http://localhost:8080/api/public/v1/firebase/callback"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Generate random secrets with:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. Database
|
|
97
|
+
|
|
98
|
+
Apply every file in `supabase/migrations/` in order — with the Supabase CLI (`supabase link` then `supabase db push`) or by running them in the SQL editor.
|
|
99
|
+
|
|
100
|
+
### 3. Auth and Google OAuth
|
|
101
|
+
|
|
102
|
+
- **Supabase → Authentication → URL Configuration:** Site URL `http://localhost:8080`; add redirect URLs `http://localhost:8080/dashboard`, `/onboarding`, `/reset-password`.
|
|
103
|
+
- **Supabase → Authentication → Providers → Google:** enable it with your OAuth client for "Continue with Google".
|
|
104
|
+
- **Google Cloud → OAuth client → Authorised redirect URIs:** add `http://localhost:8080/api/public/v1/firebase/callback` and `https://<project-ref>.supabase.co/auth/v1/callback`.
|
|
105
|
+
|
|
106
|
+
### 4. Run
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npm run dev # http://localhost:8080
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Open `/signup`, create your workspace, connect a Firebase project (Cloud Storage must be set up) and register your app.
|
|
113
|
+
|
|
114
|
+
## Deploy to Vercel
|
|
115
|
+
|
|
116
|
+
The repository includes [`vercel.json`](vercel.json). Nitro detects Vercel and builds a Node.js function plus static assets.
|
|
117
|
+
|
|
118
|
+
1. Import the repository in Vercel (no framework preset needed).
|
|
119
|
+
2. Add every variable from the `.env` section under **Project → Settings → Environment Variables**, with production URLs:
|
|
120
|
+
- `GOOGLE_REDIRECT_URI=https://<your-domain>/api/public/v1/firebase/callback`
|
|
121
|
+
- `CRON_SECRET=<random>` — Vercel sends it to the maintenance cron automatically.
|
|
122
|
+
3. Deploy, then update the production URLs in Supabase Auth and in the Google OAuth client.
|
|
123
|
+
4. Check `https://<your-domain>/health/ready` returns `{"status":"ok"}`.
|
|
124
|
+
|
|
125
|
+
The cron in `vercel.json` runs the maintenance job (webhook retries, OAuth state cleanup) daily, which works on the Hobby plan. On Pro, change the schedule to `*/5 * * * *` for faster webhook retries.
|
|
126
|
+
|
|
127
|
+
> **Upload size on Vercel:** Vercel Functions accept request bodies up to **4.5 MB**. Larger update packages must be published against a deployment without that limit (or a direct-to-Storage upload, on the roadmap).
|
|
128
|
+
|
|
129
|
+
## Install
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
npm install @fireota/react-native
|
|
133
|
+
npm install --save-dev @fireota/cli
|
|
134
|
+
cd ios && pod install
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Requires a bare React Native app, 0.73 or newer (Expo: run `npx expo prebuild`).
|
|
138
|
+
|
|
139
|
+
## Android
|
|
140
|
+
|
|
141
|
+
In `MainApplication.kt` (React Native 0.76+ template):
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
import dev.fireota.reactnative.FireOTA
|
|
145
|
+
|
|
146
|
+
override val reactHost: ReactHost by lazy {
|
|
147
|
+
getDefaultReactHost(
|
|
148
|
+
context = applicationContext,
|
|
149
|
+
packageList = PackageList(this).packages,
|
|
150
|
+
jsBundleFilePath = if (BuildConfig.DEBUG) null else FireOTA.getJSBundleFile(applicationContext),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Older templates that override `reactNativeHost`:
|
|
156
|
+
|
|
157
|
+
```kotlin
|
|
158
|
+
override fun getJSBundleFile(): String? =
|
|
159
|
+
if (BuildConfig.DEBUG) super.getJSBundleFile()
|
|
160
|
+
else FireOTA.getJSBundleFile(applicationContext) ?: super.getJSBundleFile()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## iOS
|
|
164
|
+
|
|
165
|
+
```objc
|
|
166
|
+
// AppDelegate.mm
|
|
167
|
+
#import <FireOTA.h>
|
|
168
|
+
|
|
169
|
+
- (NSURL *)bundleURL
|
|
170
|
+
{
|
|
171
|
+
#if DEBUG
|
|
172
|
+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
|
|
173
|
+
#else
|
|
174
|
+
return [FireOTA bundleURL] ?: [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
175
|
+
#endif
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Swift: add `#import <FireOTA.h>` to the bridging header and return `FireOTA.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")` from `bundleURL()` in release builds.
|
|
180
|
+
|
|
181
|
+
## Configure
|
|
182
|
+
|
|
183
|
+
Call once, early and unconditionally (e.g. `index.js`). Reaching this call confirms a new bundle starts; a bundle that never reaches it is rolled back on the next launch.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
import FireOTA from "@fireota/react-native";
|
|
187
|
+
|
|
188
|
+
if (!__DEV__) {
|
|
189
|
+
FireOTA.configure({
|
|
190
|
+
appId: "YOUR_APP_ID",
|
|
191
|
+
serverUrl: "https://ota.example.com",
|
|
192
|
+
runtimeVersion: "1.3.0", // must equal this store build; bump when native code changes
|
|
193
|
+
channel: "production",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
By default updates download silently and apply on the next cold start; mandatory updates restart immediately. To show your own prompt, set `checkOnAppStart: false` and use the API:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
const result = await FireOTA.checkForUpdate();
|
|
202
|
+
if (result.updateAvailable) {
|
|
203
|
+
// show your UI with result.release.version / result.release.notes, then:
|
|
204
|
+
await FireOTA.downloadUpdate(result.release);
|
|
205
|
+
await FireOTA.installUpdate(result.release, "immediate");
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## API
|
|
210
|
+
|
|
211
|
+
| Method | Description |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `configure(options)` | Initialize once at startup (required). |
|
|
214
|
+
| `sync()` | Check, download and install, honouring install modes. |
|
|
215
|
+
| `checkForUpdate()` | `{ updateAvailable, reason?, release? }` |
|
|
216
|
+
| `downloadUpdate(release)` | Download and verify; the running bundle is untouched. |
|
|
217
|
+
| `installUpdate(release, mode)` | `"nextRestart"` or `"immediate"`. |
|
|
218
|
+
| `reload()` | Restart now to apply a pending update. |
|
|
219
|
+
| `clearPendingUpdate()` | Drop a downloaded update that is not applied yet. |
|
|
220
|
+
| `getCurrentRelease()` | `{ releaseId, pendingReleaseId, installationId }` |
|
|
221
|
+
| `getUpdateStatus()` | Current status string. |
|
|
222
|
+
|
|
223
|
+
Options: `appId`, `serverUrl`, `runtimeVersion` (required), `channel`, `checkOnAppStart`, `installMode`, `mandatoryInstallMode`, `onStatusChange(status, detail)`.
|
|
224
|
+
|
|
225
|
+
## Publishing updates
|
|
226
|
+
|
|
227
|
+
Use [`@fireota/cli`](https://www.npmjs.com/package/@fireota/cli):
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
npx fireota publish --app YOUR_APP_ID --platform android --version 1.3.1 --runtime 1.3.0 --channel production --rollout 10
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback -> rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback }
|
|
3
|
+
repositories { google(); mavenCentral() }
|
|
4
|
+
dependencies {
|
|
5
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '2.1.20')}"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
apply plugin: "com.android.library"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AGP 9 compiles Kotlin itself ("built-in Kotlin", on by default) and registers the `kotlin`
|
|
13
|
+
* extension; applying kotlin-android on top fails with "Cannot add extension with name 'kotlin'".
|
|
14
|
+
* Only apply it on AGP 8 or when the app turned built-in Kotlin off (android.builtInKotlin=false).
|
|
15
|
+
*/
|
|
16
|
+
def needsKotlinPlugin() {
|
|
17
|
+
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
|
|
18
|
+
if (agpMajor <= 8) return true
|
|
19
|
+
def builtIn = providers.gradleProperty("android.builtInKotlin").orNull
|
|
20
|
+
return builtIn != null && !builtIn.toBoolean()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def useKotlinPlugin = needsKotlinPlugin()
|
|
24
|
+
if (useKotlinPlugin) {
|
|
25
|
+
apply plugin: "kotlin-android"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace "dev.fireota.reactnative"
|
|
30
|
+
compileSdk safeExtGet("compileSdkVersion", 35)
|
|
31
|
+
defaultConfig {
|
|
32
|
+
minSdk safeExtGet("minSdkVersion", 24)
|
|
33
|
+
}
|
|
34
|
+
compileOptions {
|
|
35
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
36
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (useKotlinPlugin) {
|
|
41
|
+
// Built-in Kotlin derives this from compileOptions; the standalone plugin needs it explicitly.
|
|
42
|
+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
|
43
|
+
kotlinOptions.jvmTarget = "17"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
repositories { google(); mavenCentral() }
|
|
48
|
+
|
|
49
|
+
dependencies {
|
|
50
|
+
implementation "com.facebook.react:react-android"
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
3
|
+
<application>
|
|
4
|
+
<activity
|
|
5
|
+
android:name="dev.fireota.reactnative.FireOTARestartActivity"
|
|
6
|
+
android:exported="false"
|
|
7
|
+
android:process=":fireota_restart"
|
|
8
|
+
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
9
|
+
</application>
|
|
10
|
+
</manifest>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package dev.fireota.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Entry point for MainApplication. Returns the path of the OTA bundle to load, or null to
|
|
7
|
+
* fall back to the bundle shipped inside the APK/AAB.
|
|
8
|
+
*
|
|
9
|
+
* override fun getJSBundleFile(): String? =
|
|
10
|
+
* FireOTA.getJSBundleFile(applicationContext) ?: super.getJSBundleFile()
|
|
11
|
+
*/
|
|
12
|
+
object FireOTA {
|
|
13
|
+
@Volatile private var resolved = false
|
|
14
|
+
@Volatile private var bundlePath: String? = null
|
|
15
|
+
|
|
16
|
+
@JvmStatic
|
|
17
|
+
fun getJSBundleFile(context: Context): String? {
|
|
18
|
+
// Resolve once per process: launch attempts are counted per process start.
|
|
19
|
+
if (!resolved) {
|
|
20
|
+
synchronized(this) {
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
bundlePath = try {
|
|
23
|
+
FireOTAStore(context.applicationContext).resolveLaunchBundle()
|
|
24
|
+
} catch (_: Throwable) {
|
|
25
|
+
null // never block startup: fall back to the embedded bundle
|
|
26
|
+
}
|
|
27
|
+
resolved = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return bundlePath
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package dev.fireota.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.os.Process
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
9
|
+
import com.facebook.react.bridge.ReactMethod
|
|
10
|
+
import java.util.concurrent.Executors
|
|
11
|
+
|
|
12
|
+
class FireOTAModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
13
|
+
private val store = FireOTAStore(reactContext)
|
|
14
|
+
private val io = Executors.newSingleThreadExecutor()
|
|
15
|
+
|
|
16
|
+
override fun getName() = "FireOTA"
|
|
17
|
+
|
|
18
|
+
@ReactMethod
|
|
19
|
+
fun getState(promise: Promise) {
|
|
20
|
+
try {
|
|
21
|
+
val map = Arguments.createMap()
|
|
22
|
+
map.putString("installationId", store.installationId)
|
|
23
|
+
map.putString("nativeVersion", store.nativeVersion())
|
|
24
|
+
map.putString("appVersion", store.nativeVersion())
|
|
25
|
+
map.putString("currentReleaseId", store.get(FireOTAStore.KEY_CURRENT))
|
|
26
|
+
map.putString("pendingReleaseId", store.get(FireOTAStore.KEY_PENDING))
|
|
27
|
+
map.putString("previousReleaseId", store.get(FireOTAStore.KEY_PREVIOUS))
|
|
28
|
+
map.putString("failedReleaseId", store.get(FireOTAStore.KEY_FAILED))
|
|
29
|
+
map.putBoolean("isFirstRun", store.isUnconfirmed())
|
|
30
|
+
promise.resolve(map)
|
|
31
|
+
} catch (e: Throwable) {
|
|
32
|
+
promise.reject("FIREOTA_STATE", e.message, e)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@ReactMethod
|
|
37
|
+
fun downloadUpdate(url: String, sha256: String, releaseId: String, promise: Promise) {
|
|
38
|
+
io.execute {
|
|
39
|
+
try {
|
|
40
|
+
store.download(url, sha256, releaseId)
|
|
41
|
+
promise.resolve(null)
|
|
42
|
+
} catch (e: SecurityException) {
|
|
43
|
+
promise.reject("FIREOTA_CHECKSUM", e.message, e)
|
|
44
|
+
} catch (e: Throwable) {
|
|
45
|
+
promise.reject("FIREOTA_DOWNLOAD", e.message, e)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactMethod
|
|
51
|
+
fun installUpdate(releaseId: String, promise: Promise) {
|
|
52
|
+
try {
|
|
53
|
+
store.setPending(releaseId)
|
|
54
|
+
promise.resolve(null)
|
|
55
|
+
} catch (e: Throwable) {
|
|
56
|
+
promise.reject("FIREOTA_INSTALL", e.message, e)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ReactMethod
|
|
61
|
+
fun markSuccess(promise: Promise) {
|
|
62
|
+
io.execute {
|
|
63
|
+
try {
|
|
64
|
+
store.markSuccess()
|
|
65
|
+
promise.resolve(null)
|
|
66
|
+
} catch (e: Throwable) {
|
|
67
|
+
promise.reject("FIREOTA_CONFIRM", e.message, e)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@ReactMethod
|
|
73
|
+
fun clearPending(promise: Promise) {
|
|
74
|
+
store.clear(FireOTAStore.KEY_PENDING)
|
|
75
|
+
store.clear(FireOTAStore.KEY_DOWNLOADED)
|
|
76
|
+
promise.resolve(null)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@ReactMethod
|
|
80
|
+
fun clearFailed(promise: Promise) {
|
|
81
|
+
store.clear(FireOTAStore.KEY_FAILED)
|
|
82
|
+
promise.resolve(null)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Restarts the process so the pending bundle loads. React Native reuses its bundle loader on
|
|
87
|
+
* a JS reload, so a cold restart is the reliable way to switch bundles on Android.
|
|
88
|
+
*/
|
|
89
|
+
@ReactMethod
|
|
90
|
+
fun restartApp(promise: Promise) {
|
|
91
|
+
val launch = reactContext.packageManager.getLaunchIntentForPackage(reactContext.packageName)
|
|
92
|
+
if (launch == null) {
|
|
93
|
+
promise.reject("FIREOTA_RESTART", "No launch activity found.")
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
promise.resolve(null)
|
|
97
|
+
val intent = Intent(reactContext, FireOTARestartActivity::class.java)
|
|
98
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
99
|
+
.putExtra(FireOTARestartActivity.EXTRA_LAUNCH, launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK))
|
|
100
|
+
.putExtra(FireOTARestartActivity.EXTRA_PID, Process.myPid())
|
|
101
|
+
reactContext.startActivity(intent)
|
|
102
|
+
reactContext.currentActivity?.finishAffinity()
|
|
103
|
+
Runtime.getRuntime().exit(0)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package dev.fireota.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
// The classic ReactPackage API is deprecated in RN 0.87 but still supported through the interop layer.
|
|
9
|
+
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
|
|
10
|
+
class FireOTAPackage : ReactPackage {
|
|
11
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(FireOTAModule(reactContext))
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package dev.fireota.reactnative
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import android.os.Process
|
|
8
|
+
|
|
9
|
+
/** Runs in its own process (":fireota_restart"): kills the app process, then relaunches it. */
|
|
10
|
+
class FireOTARestartActivity : Activity() {
|
|
11
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
12
|
+
super.onCreate(savedInstanceState)
|
|
13
|
+
Process.killProcess(intent.getIntExtra(EXTRA_PID, -1))
|
|
14
|
+
val launch: Intent? = if (Build.VERSION.SDK_INT >= 33) {
|
|
15
|
+
intent.getParcelableExtra(EXTRA_LAUNCH, Intent::class.java)
|
|
16
|
+
} else {
|
|
17
|
+
@Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_LAUNCH)
|
|
18
|
+
}
|
|
19
|
+
launch?.let { startActivity(it) }
|
|
20
|
+
finish()
|
|
21
|
+
Runtime.getRuntime().exit(0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
companion object {
|
|
25
|
+
const val EXTRA_LAUNCH = "dev.fireota.launch"
|
|
26
|
+
const val EXTRA_PID = "dev.fireota.pid"
|
|
27
|
+
}
|
|
28
|
+
}
|