@rovq/react-native 0.2.2 → 0.3.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.
package/LICENSE CHANGED
@@ -1,21 +1,44 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 moatazMahdi
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.
1
+ ROVQ Software License
2
+
3
+ Copyright (c) 2026 moatazMahdi. All rights reserved.
4
+
5
+ This software and its source code are the property of the copyright holder.
6
+ They are licensed, not sold.
7
+
8
+ 1. Permission. You may install and use this software, without changes, only
9
+ to add over-the-air updates to your own applications through a ROVQ server
10
+ that the copyright holder operates or has authorized. You may include it,
11
+ as part of your compiled application, in the copies of that application
12
+ you distribute to your users.
13
+
14
+ 2. Restrictions. Except as section 1 allows, you may not:
15
+ a. copy, modify, translate, or create derivative works of this software;
16
+ b. distribute, sublicense, sell, rent, or publish this software, or any
17
+ part of it, on its own or as part of another library, tool, or service;
18
+ c. use this software, or any part of it, to build or operate a product or
19
+ service that provides over-the-air updates, or that competes with ROVQ;
20
+ d. use this software with a server that the copyright holder neither
21
+ operates nor has authorized;
22
+ e. remove or change this license or any copyright notice.
23
+
24
+ 3. Ownership. The copyright holder keeps all rights not expressly granted
25
+ here. Nothing in this license grants any right to the ROVQ name or logo.
26
+
27
+ 4. Termination. This license ends automatically if you break any of its
28
+ terms. You must then stop using this software and delete every copy of it.
29
+
30
+ 5. No warranty. THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
31
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
33
+
34
+ 6. Limitation of liability. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE
35
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
36
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
37
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
38
+
39
+ Versions of this software published under the MIT License before this license
40
+ was adopted remain available under the MIT License. This license applies to
41
+ every later version.
42
+
43
+ For any use this license does not allow, contact the copyright holder
44
+ through https://rovq-dashboard.onrender.com.
package/README.md CHANGED
@@ -4,23 +4,29 @@ Over-the-air updates for React Native apps, served by your own ROVQ server. Each
4
4
 
5
5
  **Dashboard and docs:** [rovq-dashboard.onrender.com](https://rovq-dashboard.onrender.com). Create an app there, and its page shows this setup with your own keys.
6
6
 
7
- Requires React Native 0.82 or newer, with the New Architecture.
7
+ This is line 0.3, for React Native 0.72 to 0.81, with the New Architecture or without it. On React Native 0.82 and newer, install the newest line, `npm install @rovq/react-native@latest`, whose setup differs.
8
8
 
9
9
  ## Install
10
10
 
11
11
  ```sh
12
- npm install @rovq/react-native
12
+ npm install @rovq/react-native@0.3
13
13
  cd ios && pod install
14
14
  ```
15
15
 
16
- Then connect the native side, which decides which bundle runs. The setup page of your app in the ROVQ dashboard shows this code with your app's real values.
16
+ Keep the `0.3`: without it npm installs the newest line, which needs React Native 0.82. On React Native 0.72, also set `platform :ios, '13.4'` in the Podfile.
17
17
 
18
- ### Android, in `MainApplication.kt`
18
+ Then connect the native side, which decides which bundle runs. The docs in the ROVQ dashboard show the code for your React Native version, and your app's setup page fills in its real values.
19
+
20
+ ### Android
21
+
22
+ Return the engine's choice from `getJSBundleFile()`, and start the engine first thing in `onCreate`. In `MainApplication.kt`, from React Native 0.73:
19
23
 
20
24
  ```kotlin
21
- override val reactHost: ReactHost by lazy {
22
- RovqReactHost.create(this, PackageList(this).packages)
23
- }
25
+ override val reactNativeHost: ReactNativeHost =
26
+ object : DefaultReactNativeHost(this) {
27
+ // ...what is already here, then:
28
+ override fun getJSBundleFile(): String? = Rovq.getJSBundleFile()
29
+ }
24
30
 
25
31
  override fun onCreate() {
26
32
  super.onCreate()
@@ -33,11 +39,13 @@ override fun onCreate() {
33
39
  trustedKeys = mapOf("2026-11" to "<public key>"),
34
40
  ),
35
41
  )
36
- loadReactNative(this)
42
+ // ...what is already here.
37
43
  }
38
44
  ```
39
45
 
40
- ### iOS, in `AppDelegate.swift` and `SceneDelegate.swift`
46
+ On React Native 0.72, the same in `MainApplication.java`, with `protected String getJSBundleFile()` and `new RovqConfig(...)`.
47
+
48
+ ### iOS, from React Native 0.77, in `AppDelegate.swift`
41
49
 
42
50
  ```swift
43
51
  RovqHost.initialize(
@@ -49,6 +57,7 @@ RovqHost.initialize(
49
57
  )
50
58
  )
51
59
 
60
+ // In the class that overrides bundleURL():
52
61
  override func bundleURL() -> URL? {
53
62
  #if DEBUG
54
63
  RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
@@ -58,6 +67,26 @@ override func bundleURL() -> URL? {
58
67
  }
59
68
  ```
60
69
 
70
+ ### iOS, on React Native 0.72 to 0.76, in `AppDelegate.mm`
71
+
72
+ ```objc
73
+ #import <Rovq/RovqSetup.h>
74
+
75
+ // First thing in didFinishLaunchingWithOptions:
76
+ [RovqSetup initializeWithApiURL:@"https://your-server/api/v1"
77
+ deploymentKey:@"rovq_dep_..."
78
+ runtimeVersion:@"1"
79
+ trustedKeys:@{@"2026-11" : @"<public key>"}
80
+ allowInsecureURLs:NO];
81
+
82
+ // In bundleURL from 0.74, getBundleURL in 0.73, sourceURLForBridge: in 0.72:
83
+ #if DEBUG
84
+ return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
85
+ #else
86
+ return [RovqSetup bundleURL] ?: [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
87
+ #endif
88
+ ```
89
+
61
90
  ## Use
62
91
 
63
92
  ```ts
@@ -120,4 +149,4 @@ A release published by `@rovq/cli` 0.2 lists its files, and the SDK downloads on
120
149
 
121
150
  ## License
122
151
 
123
- MIT
152
+ Proprietary. See [LICENSE](LICENSE).
package/Rovq.podspec CHANGED
@@ -6,12 +6,16 @@ Pod::Spec.new do |s|
6
6
  s.name = 'Rovq'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
- s.homepage = 'https://github.com/moatazMahdi/rovq'
10
- s.license = 'MIT'
9
+ s.homepage = package['homepage']
10
+ s.license = { type: 'Proprietary', file: 'LICENSE' }
11
11
  s.authors = 'moatazMahdi'
12
- s.platforms = { ios: min_ios_version_supported }
12
+ # The engine uses CryptoKit and FileHandle APIs from iOS 13.4, React Native 0.73's minimum. An
13
+ # app on 0.72, which allows 12.4, sets platform :ios, '13.4' in its Podfile.
14
+ s.platforms = { ios: '13.4' }
13
15
  s.source = { git: 'https://github.com/moatazMahdi/rovq.git', tag: s.version.to_s }
14
- s.source_files = 'ios/Engine/**/*.swift', 'ios/React/**/*.{swift,mm}'
16
+ s.source_files = 'ios/Engine/**/*.swift', 'ios/React/**/*.{swift,h,mm}'
17
+ # Only this header is for apps. RovqHost reaches Swift apps through the module.
18
+ s.public_header_files = 'ios/React/RovqSetup.h'
15
19
  s.swift_version = '5.9'
16
20
  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
17
21
 
@@ -1,17 +1,29 @@
1
+ // The engine has no React Native code, so it can be built and tested on its own, in
2
+ // seconds. Inside an app, the React Native integration is added on top of it.
3
+ buildscript {
4
+ // An app on React Native 0.72 has no Kotlin plugin of its own, and apps from 0.73 have one.
5
+ // Two versions of it cannot share a build, so this uses the app's version when there is one.
6
+ if (rootProject.name != 'rovq-engine') {
7
+ def kotlinVersion = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '1.8.22'
8
+ repositories {
9
+ google()
10
+ mavenCentral()
11
+ }
12
+ dependencies {
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
14
+ }
15
+ }
16
+ }
17
+
1
18
  plugins {
2
19
  id 'com.android.library'
3
20
  }
4
21
 
5
- // The engine has no React Native code, so it can be built and tested on its own, in
6
- // seconds. Inside an app, the React Native integration is added on top of it.
7
22
  def insideApp = rootProject.name != 'rovq-engine'
8
23
 
9
- // Android Gradle Plugin 9 compiles Kotlin itself, unless an app turns that off, as React Native's
10
- // template does. Earlier versions, used up to React Native 0.87, need the Kotlin plugin, or the
11
- // Kotlin sources are left out and the app cannot find any of these classes.
12
- def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
13
- def usesKotlinPlugin = insideApp &&
14
- (agpMajor < 9 || findProperty('android.builtInKotlin') == 'false')
24
+ // React Native 0.72 to 0.81 build with Android Gradle Plugin 8, which compiles Kotlin only through
25
+ // the Kotlin plugin. Without it, the Kotlin sources are left out and the app cannot find them.
26
+ def usesKotlinPlugin = insideApp
15
27
 
16
28
  // Rebuilding only changed files fails with "Cannot access ... it is internal" when this package is
17
29
  // linked through a symbolic link, as npm's file: dependencies are. Compiling it fully each time
@@ -22,30 +34,29 @@ if (insideApp) {
22
34
 
23
35
  if (usesKotlinPlugin) {
24
36
  apply plugin: 'kotlin-android'
37
+ // Java compiles for 17 below, and Kotlin 1.8 stops the build when the two differ.
38
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
39
+ kotlinOptions.jvmTarget = '17'
40
+ }
25
41
  }
26
42
 
27
- // Keeps the two kinds of build from mixing outputs. The build inside an app stays in build/,
28
- // because React Native's autolinking expects the generated code there.
43
+ // Keeps the two kinds of build from mixing outputs.
29
44
  if (!insideApp) {
30
45
  layout.buildDirectory = layout.projectDirectory.dir('build/standalone')
31
46
  }
32
47
 
33
- if (insideApp) {
34
- apply plugin: 'com.facebook.react'
35
-
36
- react {
37
- // The app's install holds React Native, not this package's folder.
38
- reactNativeDir = rootProject.file('../node_modules/react-native')
39
- codegenDir = rootProject.file('../node_modules/@react-native/codegen')
40
- }
48
+ def appValue = { String name, Object fallback ->
49
+ rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
41
50
  }
42
51
 
43
52
  android {
44
53
  namespace = 'com.rovq.engine'
45
- compileSdk = 37
54
+ // An app's own compileSdk, because older Android Gradle Plugins cannot read newer platforms.
55
+ compileSdk = insideApp ? appValue('compileSdkVersion', 36) : 37
46
56
 
47
57
  defaultConfig {
48
- minSdk = 24
58
+ // React Native 0.72 to 0.75 apps start at API 21.
59
+ minSdk = 21
49
60
  testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'
50
61
  }
51
62
 
@@ -57,8 +68,6 @@ android {
57
68
  sourceSets {
58
69
  if (usesKotlinPlugin) {
59
70
  main.java.srcDirs += 'src/react/java'
60
- } else if (insideApp) {
61
- main.kotlin.directories.add('src/react/java')
62
71
  } else {
63
72
  // Test helpers that run both on the computer and on a device.
64
73
  test.kotlin.directories.add('src/sharedTest/java')
@@ -73,11 +82,15 @@ android {
73
82
  }
74
83
 
75
84
  dependencies {
76
- // Android includes Ed25519 only from API 33, and the engine supports API 24.
85
+ // Android includes Ed25519 only from API 33, and the engine supports API 21.
77
86
  implementation 'com.google.crypto.tink:tink-android:1.24.0'
87
+ // Tink brings androidx.annotation 1.8, which moved its classes to annotation-jvm, and React
88
+ // Native 0.72 brings 1.3. Asking for 1.8 here lifts the old one, so the classes are not twice
89
+ // in the app.
90
+ implementation 'androidx.annotation:annotation:1.8.2'
78
91
 
79
92
  if (insideApp) {
80
- // The version is set by the React Native Gradle Plugin.
93
+ // The version is set by the React Native Gradle Plugin of the app.
81
94
  implementation 'com.facebook.react:react-android'
82
95
  }
83
96
 
@@ -49,13 +49,13 @@ data class UpdateInfo(
49
49
  )
50
50
 
51
51
  sealed interface CheckResult {
52
- data object UpToDate : CheckResult
52
+ object UpToDate : CheckResult
53
53
 
54
54
  /** @param alreadyDownloaded whether [RovqEngine.downloadUpdate] has nothing left to fetch. */
55
55
  data class UpdateAvailable(val update: UpdateInfo, val alreadyDownloaded: Boolean) : CheckResult
56
56
 
57
57
  /** The server asked to leave the current release. Restart to apply. */
58
- data object RolledBack : CheckResult
58
+ object RolledBack : CheckResult
59
59
 
60
60
  data class Failed(val code: RovqErrorCode, val message: String) : CheckResult
61
61
  }
@@ -75,13 +75,13 @@ sealed interface InstallResult {
75
75
  }
76
76
 
77
77
  sealed interface SyncResult {
78
- data object UpToDate : SyncResult
78
+ object UpToDate : SyncResult
79
79
 
80
80
  /** Installed, and loads on the next JavaScript start. */
81
81
  data class UpdateReady(val releaseId: String, val mandatory: Boolean) : SyncResult
82
82
 
83
83
  /** The server asked to leave the current release. Restart to apply. */
84
- data object RolledBack : SyncResult
84
+ object RolledBack : SyncResult
85
85
 
86
86
  data class Failed(val code: RovqErrorCode, val message: String) : SyncResult
87
87
  }
@@ -1,5 +1,6 @@
1
1
  package com.rovq.engine
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import java.io.File
4
5
  import java.io.IOException
5
6
  import java.io.InputStream
@@ -328,6 +329,9 @@ class UpdateInstaller(
328
329
  * A hard link costs no space. Files in a release never change, so sharing them is safe.
329
330
  * Android before 8.0 has no java.nio.file, so it copies instead.
330
331
  */
332
+ // Lint wants a version check, but SDK_INT is 0 in unit tests on the computer, which would
333
+ // stop them testing the link. Before 8.0 the call throws a LinkageError, caught below.
334
+ @SuppressLint("NewApi")
331
335
  private fun link(source: File, target: File) {
332
336
  val linked = try {
333
337
  java.nio.file.Files.createLink(target.toPath(), source.toPath())
@@ -22,8 +22,8 @@ data class CheckRequest(
22
22
 
23
23
  /** The server's answer. It is advice only, because [OfferedUpdate] is verified before use. */
24
24
  sealed interface CheckResponse {
25
- data object None : CheckResponse
26
- data object Rollback : CheckResponse
25
+ object None : CheckResponse
26
+ object Rollback : CheckResponse
27
27
  data class Update(val offer: OfferedUpdate) : CheckResponse
28
28
  }
29
29
 
@@ -12,8 +12,11 @@ import java.io.File
12
12
 
13
13
  private const val LOG_TAG = "Rovq"
14
14
 
15
- /** What an app supplies. Everything here ships inside the app, so nothing here is secret. */
16
- data class RovqConfig(
15
+ /**
16
+ * What an app supplies. Everything here ships inside the app, so nothing here is secret.
17
+ * JvmOverloads lets an app written in Java leave out allowInsecureUrls.
18
+ */
19
+ data class RovqConfig @JvmOverloads constructor(
17
20
  val apiUrl: String,
18
21
  val deploymentKey: String,
19
22
  /** Change this whenever native code changes. */
@@ -41,6 +44,7 @@ object Rovq {
41
44
  * starts, on the bundle inside it, and accepts no update. A typo must not crash every user's
42
45
  * app. The reason is logged under the tag Rovq. iOS behaves the same.
43
46
  */
47
+ @JvmStatic
44
48
  @Synchronized
45
49
  fun initialize(application: Application, config: RovqConfig) {
46
50
  if (instance != null) return
@@ -68,6 +72,51 @@ object Rovq {
68
72
  watchForCrashes(engine)
69
73
  }
70
74
 
75
+ /** What this JavaScript start runs. Unset until the first answer. */
76
+ private var chosen: Choice? = null
77
+
78
+ private class Choice(val path: String?)
79
+
80
+ /**
81
+ * Return this from ReactNativeHost.getJSBundleFile(). It is the update's bundle, or null for
82
+ * the bundle inside the app, which is what React Native loads when the answer is null.
83
+ *
84
+ * Choosing starts a pending update, so a second choice in the same start would count the
85
+ * first as failed and roll it back. The answer is therefore kept, because apps ask more than
86
+ * once: on React Native 0.76 and later, the template's reactHost getter asks on every read.
87
+ * Only [chooseAgain] chooses again, when RovqModule restarts JavaScript.
88
+ */
89
+ @JvmStatic
90
+ @Synchronized
91
+ fun getJSBundleFile(): String? {
92
+ chosen?.let { return it.path }
93
+ return chooseAgain()
94
+ }
95
+
96
+ /** Chooses the bundle for a new JavaScript start. */
97
+ @Synchronized
98
+ internal fun chooseAgain(): String? {
99
+ val path = choose()
100
+ chosen = Choice(path)
101
+ return path
102
+ }
103
+
104
+ private fun choose(): String? {
105
+ val file = try {
106
+ instance?.resolveBundle()
107
+ } catch (error: Throwable) {
108
+ // Whatever goes wrong in the engine, the app must still start.
109
+ Log.e(LOG_TAG, "Falling back to the bundle inside the app", error)
110
+ null
111
+ }
112
+ if (file == null) {
113
+ Log.i(LOG_TAG, "Loading the bundle inside the app")
114
+ return null
115
+ }
116
+ Log.i(LOG_TAG, "Loading ${file.absolutePath}")
117
+ return file.absolutePath
118
+ }
119
+
71
120
  private fun watchForCrashes(engine: RovqEngine) {
72
121
  val previous = Thread.getDefaultUncaughtExceptionHandler()
73
122
  Thread.setDefaultUncaughtExceptionHandler { thread, error ->
@@ -1,25 +1,34 @@
1
1
  package com.rovq.engine.react
2
2
 
3
+ import android.content.Intent
3
4
  import android.util.Log
4
5
  import com.facebook.react.ReactApplication
6
+ import com.facebook.react.ReactInstanceManager
5
7
  import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.JSBundleLoader
6
9
  import com.facebook.react.bridge.Promise
7
10
  import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
12
+ import com.facebook.react.bridge.ReactMethod
8
13
  import com.facebook.react.bridge.UiThreadUtil
9
14
  import com.facebook.react.bridge.WritableMap
10
- import com.facebook.react.module.annotations.ReactModule
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
11
16
  import com.rovq.engine.CheckResult
12
17
  import com.rovq.engine.DownloadResult
13
18
  import com.rovq.engine.InstallResult
19
+ import com.rovq.engine.ProgressThrottle
14
20
  import com.rovq.engine.RovqEngine
15
21
  import com.rovq.engine.RovqErrorCode
16
- import com.rovq.engine.ProgressThrottle
17
22
  import com.rovq.engine.SyncResult
18
23
  import com.rovq.engine.UpdateInfo
24
+ import java.lang.reflect.Field
19
25
  import java.util.concurrent.Executors
20
26
 
21
- @ReactModule(name = RovqModule.NAME)
22
- class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
27
+ // What React Native loads when getJSBundleFile() returns null, unless an app renames it.
28
+ private const val EMBEDDED_BUNDLE = "assets://index.android.bundle"
29
+
30
+ /** A legacy native module, which React Native 0.72 to 0.81 all run. */
31
+ class RovqModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
23
32
  // Network and disk work must stay off the JavaScript thread.
24
33
  private val worker = Executors.newSingleThreadExecutor()
25
34
 
@@ -29,7 +38,8 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
29
38
 
30
39
  override fun getName(): String = NAME
31
40
 
32
- override fun checkForUpdate(promise: Promise) = run(promise) { engine ->
41
+ @ReactMethod
42
+ fun checkForUpdate(promise: Promise) = run(promise) { engine ->
33
43
  when (val result = engine.checkForUpdate()) {
34
44
  CheckResult.UpToDate -> outcome("UP_TO_DATE")
35
45
  CheckResult.RolledBack -> outcome("ROLLED_BACK")
@@ -41,21 +51,24 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
41
51
  }
42
52
  }
43
53
 
44
- override fun downloadUpdate(promise: Promise) = run(promise) { engine ->
54
+ @ReactMethod
55
+ fun downloadUpdate(promise: Promise) = run(promise) { engine ->
45
56
  when (val result = engine.downloadUpdate(progressReporter())) {
46
57
  is DownloadResult.Downloaded -> outcome("DOWNLOADED").apply { putMap("update", toMap(result.update)) }
47
58
  is DownloadResult.Failed -> failure(result.code, result.message)
48
59
  }
49
60
  }
50
61
 
51
- override fun installUpdate(promise: Promise) = run(promise) { engine ->
62
+ @ReactMethod
63
+ fun installUpdate(promise: Promise) = run(promise) { engine ->
52
64
  when (val result = engine.installUpdate()) {
53
65
  is InstallResult.Installed -> outcome("INSTALLED").apply { putString("releaseId", result.releaseId) }
54
66
  is InstallResult.Failed -> failure(result.code, result.message)
55
67
  }
56
68
  }
57
69
 
58
- override fun sync(promise: Promise) = run(promise) { engine ->
70
+ @ReactMethod
71
+ fun sync(promise: Promise) = run(promise) { engine ->
59
72
  when (val result = engine.sync(progressReporter())) {
60
73
  SyncResult.UpToDate -> outcome("UP_TO_DATE")
61
74
  SyncResult.RolledBack -> outcome("ROLLED_BACK")
@@ -67,18 +80,114 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
67
80
  }
68
81
  }
69
82
 
70
- override fun notifyApplicationReady() {
83
+ @ReactMethod
84
+ fun notifyApplicationReady() {
71
85
  Rovq.engineOrNull?.notifyReady()
72
86
  }
73
87
 
74
- override fun restartApp() {
88
+ @ReactMethod
89
+ fun restartApp() {
75
90
  UiThreadUtil.runOnUiThread {
76
- val application = reactApplicationContext.applicationContext as ReactApplication
77
- application.reactHost?.reload("ROVQ restart")
91
+ val bridgeless = reactApplicationContext.isBridgeless
92
+ // Found before the engine is asked, because asking starts trying the pending
93
+ // release. A process that ended then would count as a failed start, and the
94
+ // release would be abandoned, though it never ran.
95
+ val target = if (bridgeless) bridgelessTarget() else bridgeTarget()
96
+ if (target == null) {
97
+ restartProcess()
98
+ return@runOnUiThread
99
+ }
100
+ // Not getJSBundleFile(), which keeps the answer of the start that is ending.
101
+ val file = Rovq.chooseAgain()
102
+ val loader = if (file != null) {
103
+ JSBundleLoader.createFileLoader(file)
104
+ } else {
105
+ // Loaded the way each kind of host loads it by default.
106
+ JSBundleLoader.createAssetLoader(reactApplicationContext, EMBEDDED_BUNDLE, bridgeless)
107
+ }
108
+ try {
109
+ target.field.set(target.owner, loader)
110
+ } catch (error: Exception) {
111
+ // Unlikely once the field is accessible. The release then counts as a failed
112
+ // start, which is the safe way to lose it.
113
+ Log.w(NAME, "Could not replace the bundle loader, so the whole app restarts", error)
114
+ restartProcess()
115
+ return@runOnUiThread
116
+ }
117
+ target.reload()
118
+ }
119
+ }
120
+
121
+ /** Where a host keeps its bundle loader, and how it starts JavaScript again. */
122
+ private class LoaderTarget(val owner: Any, val field: Field, val reload: () -> Unit)
123
+
124
+ /**
125
+ * Without the New Architecture's bridgeless mode, ReactInstanceManager reads
126
+ * getJSBundleFile() once, when it is created, and keeps a loader in a private field.
127
+ * Recreating the context alone would load the old bundle again, so restartApp puts a loader
128
+ * for the bundle the engine chooses now in that field first, as CodePush did.
129
+ */
130
+ private fun bridgeTarget(): LoaderTarget? {
131
+ val manager = (reactApplicationContext.applicationContext as ReactApplication)
132
+ .reactNativeHost.reactInstanceManager
133
+ // The Java source names it mBundleLoader. A conversion to Kotlin would drop the m.
134
+ val field = findField(ReactInstanceManager::class.java, "mBundleLoader", "bundleLoader") ?: return null
135
+ return LoaderTarget(manager, field) { manager.recreateReactContextInBackground() }
136
+ }
137
+
138
+ /**
139
+ * Bridgeless mode, the default of the New Architecture from React Native 0.76, gives
140
+ * getJSBundleFile() to DefaultReactHostDelegate, which keeps a loader in a field. The host
141
+ * asks its delegate for that loader each time JavaScript starts, so replacing it before a
142
+ * reload is enough. Reflection throughout, because ReactApplication.getReactHost does not
143
+ * exist in React Native 0.72, and this file must compile there too.
144
+ */
145
+ private fun bridgelessTarget(): LoaderTarget? = try {
146
+ val application = reactApplicationContext.applicationContext
147
+ val host = application.javaClass.getMethod("getReactHost").invoke(application)
148
+ val delegate = host?.let { findField(it.javaClass, "mReactHostDelegate", "reactHostDelegate")?.get(it) }
149
+ val field = delegate?.let { findField(it.javaClass, "jsBundleLoader", "mJsBundleLoader") }
150
+ val reload = host?.javaClass?.getMethod("reload", String::class.java)
151
+ if (host == null || delegate == null || field == null || reload == null) {
152
+ null
153
+ } else {
154
+ LoaderTarget(delegate, field) { reload.invoke(host, "ROVQ restart") }
78
155
  }
156
+ } catch (error: Exception) {
157
+ Log.w(NAME, "Could not reach the React host, so the whole app restarts", error)
158
+ null
79
159
  }
80
160
 
81
- override fun rollback(promise: Promise) {
161
+ /** Searches the class and its parents. Null if this React Native has no such field. */
162
+ private fun findField(type: Class<*>, vararg names: String): Field? {
163
+ var current: Class<*>? = type
164
+ while (current != null) {
165
+ for (name in names) {
166
+ try {
167
+ return current.getDeclaredField(name).apply { isAccessible = true }
168
+ } catch (missing: NoSuchFieldException) {
169
+ // Try the next name. Not `_`, which Kotlin 1.8 does not accept here.
170
+ }
171
+ }
172
+ current = current.superclass
173
+ }
174
+ Log.w(NAME, "${type.name} has no field ${names.joinToString(" or ")}, so the whole app restarts")
175
+ return null
176
+ }
177
+
178
+ /** Slower than a reload, but React Native creates everything again, so it always works. */
179
+ private fun restartProcess() {
180
+ val context = reactApplicationContext.applicationContext
181
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
182
+ if (intent != null) {
183
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
184
+ context.startActivity(intent)
185
+ }
186
+ Runtime.getRuntime().exit(0)
187
+ }
188
+
189
+ @ReactMethod
190
+ fun rollback(promise: Promise) {
82
191
  worker.execute {
83
192
  try {
84
193
  promise.resolve(Rovq.engineOrNull?.rollback() ?: false)
@@ -88,7 +197,8 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
88
197
  }
89
198
  }
90
199
 
91
- override fun getStatus(promise: Promise) {
200
+ @ReactMethod
201
+ fun getStatus(promise: Promise) {
92
202
  val status = Rovq.engineOrNull?.status()
93
203
  promise.resolve(
94
204
  Arguments.createMap().apply {
@@ -104,6 +214,13 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
104
214
  )
105
215
  }
106
216
 
217
+ // NativeEventEmitter calls these. Events are sent whether anyone listens or not.
218
+ @ReactMethod
219
+ fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) = Unit
220
+
221
+ @ReactMethod
222
+ fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Double) = Unit
223
+
107
224
  private fun run(promise: Promise, work: (RovqEngine) -> WritableMap) {
108
225
  worker.execute {
109
226
  try {
@@ -123,13 +240,16 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
123
240
 
124
241
  private fun progressReporter(): (Long, Long) -> Unit {
125
242
  val throttle = ProgressThrottle { received, total ->
126
- if (!invalidated) {
127
- emitOnDownloadProgress(
128
- Arguments.createMap().apply {
129
- putDouble("receivedBytes", received.toDouble())
130
- putDouble("totalBytes", total.toDouble())
131
- },
132
- )
243
+ if (!invalidated && reactApplicationContext.hasActiveReactInstance()) {
244
+ reactApplicationContext
245
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
246
+ .emit(
247
+ PROGRESS_EVENT,
248
+ Arguments.createMap().apply {
249
+ putDouble("receivedBytes", received.toDouble())
250
+ putDouble("totalBytes", total.toDouble())
251
+ },
252
+ )
133
253
  }
134
254
  }
135
255
  return throttle::report
@@ -160,5 +280,8 @@ class RovqModule(context: ReactApplicationContext) : NativeRovqSpec(context) {
160
280
 
161
281
  companion object {
162
282
  const val NAME = "Rovq"
283
+
284
+ /** The same name as DOWNLOAD_PROGRESS_EVENT in src/NativeRovq.ts. */
285
+ const val PROGRESS_EVENT = "RovqDownloadProgress"
163
286
  }
164
287
  }
@@ -0,0 +1,25 @@
1
+ package com.rovq.engine.react;
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
+ import java.util.Collections;
8
+ import java.util.List;
9
+
10
+ /**
11
+ * Found by React Native autolinking. Java, not Kotlin, because ReactPackage moved from Java to
12
+ * Kotlin within the versions this line supports, and only a Java class overrides both.
13
+ */
14
+ public class RovqPackage implements ReactPackage {
15
+ @Override
16
+ public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
17
+ return Collections.singletonList(new RovqModule(reactContext));
18
+ }
19
+
20
+ @Override
21
+ @SuppressWarnings("rawtypes")
22
+ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
23
+ return Collections.emptyList();
24
+ }
25
+ }
@@ -65,6 +65,27 @@ public final class RovqHost: NSObject {
65
65
  }
66
66
  }
67
67
 
68
+ /// The same, for an AppDelegate written in Objective-C, as React Native's templates were until
69
+ /// 0.77. A Swift struct such as RovqConfig cannot cross into Objective-C.
70
+ @objc(initializeWithApiURL:deploymentKey:runtimeVersion:trustedKeys:allowInsecureURLs:)
71
+ public static func initialize(
72
+ apiURL: String,
73
+ deploymentKey: String,
74
+ runtimeVersion: String,
75
+ trustedKeys: [String: String],
76
+ allowInsecureURLs: Bool
77
+ ) {
78
+ initialize(
79
+ config: RovqConfig(
80
+ apiURL: apiURL,
81
+ deploymentKey: deploymentKey,
82
+ runtimeVersion: runtimeVersion,
83
+ trustedKeys: trustedKeys,
84
+ allowInsecureURLs: allowInsecureURLs
85
+ )
86
+ )
87
+ }
88
+
68
89
  /// An app with scenes gets the scene notification, and one without gets the app one. With
69
90
  /// several windows, only the last one leaving counts as leaving the app.
70
91
  private static func watchForBackground() {
@@ -82,9 +103,9 @@ public final class RovqHost: NSObject {
82
103
  ]
83
104
  }
84
105
 
85
- /// React Native calls this each time JavaScript starts, including on reload. Returns nil
86
- /// for the bundle inside the app.
87
- public static func bundleURL() -> URL? {
106
+ /// Return this from sourceURLForBridge: or bundleURL, which React Native calls each time
107
+ /// JavaScript starts, including on reload. Returns nil for the bundle inside the app.
108
+ @objc public static func bundleURL() -> URL? {
88
109
  guard let engine else { return nil }
89
110
  let bundle = engine.resolveBundle()
90
111
  if engine.status().tryingReleaseId != nil {
@@ -1,8 +1,9 @@
1
- // Objective-C++ because a TurboModule must return a C++ object. The work happens in Swift.
1
+ // A legacy native module, which React Native 0.72 to 0.81 all run, with or without the New
2
+ // Architecture. The work happens in Swift.
2
3
  #import <Foundation/Foundation.h>
3
- #import <RovqSpec/RovqSpec.h>
4
+ #import <React/RCTBridgeModule.h>
5
+ #import <React/RCTEventEmitter.h>
4
6
  #import <React/RCTReloadCommand.h>
5
- #import <React/RCTUtils.h>
6
7
 
7
8
  #if __has_include(<Rovq/Rovq-Swift.h>)
8
9
  #import <Rovq/Rovq-Swift.h>
@@ -10,47 +11,66 @@
10
11
  #import "Rovq-Swift.h"
11
12
  #endif
12
13
 
13
- @interface RovqModule : NativeRovqSpecBase <NativeRovqSpec>
14
+ // The same name as DOWNLOAD_PROGRESS_EVENT in src/NativeRovq.ts.
15
+ static NSString *const RovqDownloadProgressEvent = @"RovqDownloadProgress";
16
+
17
+ @interface RovqModule : RCTEventEmitter <RCTBridgeModule>
14
18
  @end
15
19
 
16
- @implementation RovqModule
20
+ @implementation RovqModule {
21
+ BOOL _hasListeners;
22
+ }
17
23
 
18
24
  RCT_EXPORT_MODULE(Rovq)
19
25
 
20
- - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
21
- (const facebook::react::ObjCTurboModule::InitParams &)params
26
+ + (BOOL)requiresMainQueueSetup
27
+ {
28
+ return NO;
29
+ }
30
+
31
+ - (NSArray<NSString *> *)supportedEvents
32
+ {
33
+ return @[ RovqDownloadProgressEvent ];
34
+ }
35
+
36
+ - (void)startObserving
37
+ {
38
+ _hasListeners = YES;
39
+ }
40
+
41
+ - (void)stopObserving
22
42
  {
23
- return std::make_shared<facebook::react::NativeRovqSpecJSI>(params);
43
+ _hasListeners = NO;
24
44
  }
25
45
 
26
- - (void)checkForUpdate:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
46
+ RCT_EXPORT_METHOD(checkForUpdate : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
27
47
  {
28
48
  [RovqHost checkForUpdateWithCompletion:^(NSDictionary<NSString *, id> *result) {
29
49
  resolve(result);
30
50
  }];
31
51
  }
32
52
 
33
- - (void)downloadUpdate:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
53
+ RCT_EXPORT_METHOD(downloadUpdate : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
34
54
  {
35
55
  [RovqHost downloadUpdateOnProgress:[self progressReporter]
36
- completion:^(NSDictionary<NSString *, id> *result) {
37
- resolve(result);
38
- }];
56
+ completion:^(NSDictionary<NSString *, id> *result) {
57
+ resolve(result);
58
+ }];
39
59
  }
40
60
 
41
- - (void)installUpdate:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
61
+ RCT_EXPORT_METHOD(installUpdate : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
42
62
  {
43
63
  [RovqHost installUpdateWithCompletion:^(NSDictionary<NSString *, id> *result) {
44
64
  resolve(result);
45
65
  }];
46
66
  }
47
67
 
48
- - (void)sync:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
68
+ RCT_EXPORT_METHOD(sync : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
49
69
  {
50
70
  [RovqHost syncOnProgress:[self progressReporter]
51
- completion:^(NSDictionary<NSString *, id> *result) {
52
- resolve(result);
53
- }];
71
+ completion:^(NSDictionary<NSString *, id> *result) {
72
+ resolve(result);
73
+ }];
54
74
  }
55
75
 
56
76
  // Weak, because a download keeps running through a reload, when this module has gone.
@@ -58,16 +78,22 @@ RCT_EXPORT_MODULE(Rovq)
58
78
  {
59
79
  __weak RovqModule *weakSelf = self;
60
80
  return ^(int64_t received, int64_t total) {
61
- [weakSelf emitOnDownloadProgress:@{@"receivedBytes" : @(received), @"totalBytes" : @(total)}];
81
+ RovqModule *strongSelf = weakSelf;
82
+ // Sending with no listener only logs a warning, so it is skipped.
83
+ if (strongSelf == nil || !strongSelf->_hasListeners) {
84
+ return;
85
+ }
86
+ [strongSelf sendEventWithName:RovqDownloadProgressEvent
87
+ body:@{@"receivedBytes" : @(received), @"totalBytes" : @(total)}];
62
88
  };
63
89
  }
64
90
 
65
- - (void)notifyApplicationReady
91
+ RCT_EXPORT_METHOD(notifyApplicationReady)
66
92
  {
67
93
  [RovqHost notifyReady];
68
94
  }
69
95
 
70
- - (void)restartApp
96
+ RCT_EXPORT_METHOD(restartApp)
71
97
  {
72
98
  // Always later, never at once: RCTExecuteOnMainQueue runs immediately when already on the main
73
99
  // thread, which reloads JavaScript while it is still inside this call.
@@ -76,14 +102,14 @@ RCT_EXPORT_MODULE(Rovq)
76
102
  });
77
103
  }
78
104
 
79
- - (void)rollback:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
105
+ RCT_EXPORT_METHOD(rollback : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
80
106
  {
81
107
  [RovqHost rollbackWithCompletion:^(BOOL result) {
82
108
  resolve(@(result));
83
109
  }];
84
110
  }
85
111
 
86
- - (void)getStatus:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
112
+ RCT_EXPORT_METHOD(getStatus : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
87
113
  {
88
114
  resolve([RovqHost status]);
89
115
  }
@@ -0,0 +1,27 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// For an AppDelegate written in Objective-C, as React Native's templates were until 0.77. Such a
6
+ /// file cannot import the header Swift generates for this pod, so these call RovqHost for it.
7
+ /// An AppDelegate written in Swift uses RovqHost directly.
8
+ @interface RovqSetup : NSObject
9
+
10
+ /// Must run before React Native starts. If the settings are unusable, the app still runs the
11
+ /// bundle inside it, and no update is ever accepted.
12
+ + (void)initializeWithApiURL:(NSString *)apiURL
13
+ deploymentKey:(NSString *)deploymentKey
14
+ runtimeVersion:(NSString *)runtimeVersion
15
+ trustedKeys:(NSDictionary<NSString *, NSString *> *)trustedKeys
16
+ allowInsecureURLs:(BOOL)allowInsecureURLs;
17
+
18
+ /// Return this from sourceURLForBridge: or bundleURL. Nil means the bundle inside the app.
19
+ + (nullable NSURL *)bundleURL;
20
+
21
+ /// Goes back one release, as rollback() does in JavaScript. The completion says whether there
22
+ /// was one to leave, on a background queue. It applies when JavaScript next starts.
23
+ + (void)rollbackWithCompletion:(void (^)(BOOL rolledBack))completion;
24
+
25
+ @end
26
+
27
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
1
+ #import "RovqSetup.h"
2
+
3
+ #if __has_include(<Rovq/Rovq-Swift.h>)
4
+ #import <Rovq/Rovq-Swift.h>
5
+ #else
6
+ #import "Rovq-Swift.h"
7
+ #endif
8
+
9
+ @implementation RovqSetup
10
+
11
+ + (void)initializeWithApiURL:(NSString *)apiURL
12
+ deploymentKey:(NSString *)deploymentKey
13
+ runtimeVersion:(NSString *)runtimeVersion
14
+ trustedKeys:(NSDictionary<NSString *, NSString *> *)trustedKeys
15
+ allowInsecureURLs:(BOOL)allowInsecureURLs
16
+ {
17
+ [RovqHost initializeWithApiURL:apiURL
18
+ deploymentKey:deploymentKey
19
+ runtimeVersion:runtimeVersion
20
+ trustedKeys:trustedKeys
21
+ allowInsecureURLs:allowInsecureURLs];
22
+ }
23
+
24
+ + (nullable NSURL *)bundleURL
25
+ {
26
+ return [RovqHost bundleURL];
27
+ }
28
+
29
+ + (void)rollbackWithCompletion:(void (^)(BOOL rolledBack))completion
30
+ {
31
+ [RovqHost rollbackWithCompletion:completion];
32
+ }
33
+
34
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovq/react-native",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Over-the-air updates for React Native, with signed releases and automatic rollback",
5
5
  "keywords": [
6
6
  "react-native",
@@ -11,15 +11,9 @@
11
11
  "hot-update",
12
12
  "rollback"
13
13
  ],
14
- "license": "MIT",
14
+ "license": "SEE LICENSE IN LICENSE",
15
15
  "author": "moatazMahdi",
16
16
  "homepage": "https://rovq-dashboard.onrender.com",
17
- "bugs": "https://github.com/moatazMahdi/rovq/issues",
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/moatazMahdi/rovq.git",
21
- "directory": "packages/react-native"
22
- },
23
17
  "main": "src/index.ts",
24
18
  "files": [
25
19
  "src",
@@ -35,7 +29,7 @@
35
29
  ],
36
30
  "peerDependencies": {
37
31
  "react": "*",
38
- "react-native": ">=0.82"
32
+ "react-native": ">=0.72 <0.82"
39
33
  },
40
34
  "peerDependenciesMeta": {
41
35
  "react-native": {
@@ -45,18 +39,10 @@
45
39
  "optional": true
46
40
  }
47
41
  },
48
- "codegenConfig": {
49
- "name": "RovqSpec",
50
- "type": "modules",
51
- "jsSrcsDir": "src",
52
- "android": {
53
- "javaPackageName": "com.rovq.engine.react"
54
- },
55
- "ios": {
56
- "modulesProvider": {
57
- "Rovq": "RovqModule"
58
- }
59
- }
42
+ "scripts": {
43
+ "test": "jest",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'rn-0.3') { console.error('Line 0.3 must not become latest. Publish with: npm publish --tag rn-0.3'); process.exit(1) }\""
60
46
  },
61
47
  "devDependencies": {
62
48
  "@swc/core": "1.16.22",
@@ -72,10 +58,7 @@
72
58
  "react-native": "src/index.ts",
73
59
  "types": "src/index.ts",
74
60
  "publishConfig": {
75
- "access": "public"
76
- },
77
- "scripts": {
78
- "test": "jest",
79
- "typecheck": "tsc --noEmit"
61
+ "access": "public",
62
+ "tag": "rn-0.3"
80
63
  }
81
- }
64
+ }
package/src/NativeRovq.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { CodegenTypes, TurboModule } from 'react-native';
2
- import { TurboModuleRegistry } from 'react-native';
1
+ import { NativeEventEmitter, NativeModules } from 'react-native';
2
+ import type { EmitterSubscription } from 'react-native';
3
3
 
4
- // React Native generates the native base classes from this file, and its code generator cannot
5
- // express a union of object shapes. So these are flat, and index.ts turns them into the types
6
- // that apps use. Only `react-native` and this file's own types may appear here.
4
+ // React Native 0.72 to 0.81 find this module through NativeModules, which works with or without
5
+ // the New Architecture. The main line, for 0.82 and newer, generates a TurboModule instead.
6
+ // These shapes are the same on both lines, so index.ts does not depend on which one it runs on.
7
7
 
8
8
  export interface NativeUpdate {
9
9
  releaseId: string;
@@ -57,7 +57,10 @@ export interface NativeDownloadProgress {
57
57
  totalBytes: number;
58
58
  }
59
59
 
60
- export interface Spec extends TurboModule {
60
+ /** The name both native modules send download progress under. */
61
+ export const DOWNLOAD_PROGRESS_EVENT = 'RovqDownloadProgress';
62
+
63
+ interface LegacyModule {
61
64
  checkForUpdate(): Promise<NativeCheckResult>;
62
65
  downloadUpdate(): Promise<NativeDownloadResult>;
63
66
  installUpdate(): Promise<NativeInstallResult>;
@@ -66,7 +69,48 @@ export interface Spec extends TurboModule {
66
69
  restartApp(): void;
67
70
  rollback(): Promise<boolean>;
68
71
  getStatus(): Promise<NativeStatus>;
69
- readonly onDownloadProgress: CodegenTypes.EventEmitter<NativeDownloadProgress>;
72
+ // NativeEventEmitter calls these to tell the native side when someone is listening.
73
+ addListener(eventName: string): void;
74
+ removeListeners(count: number): void;
75
+ }
76
+
77
+ export interface Spec {
78
+ checkForUpdate(): Promise<NativeCheckResult>;
79
+ downloadUpdate(): Promise<NativeDownloadResult>;
80
+ installUpdate(): Promise<NativeInstallResult>;
81
+ sync(): Promise<NativeSyncResult>;
82
+ notifyApplicationReady(): void;
83
+ restartApp(): void;
84
+ rollback(): Promise<boolean>;
85
+ getStatus(): Promise<NativeStatus>;
86
+ onDownloadProgress(listener: (progress: NativeDownloadProgress) => void): EmitterSubscription;
87
+ }
88
+
89
+ function load(): Spec {
90
+ const module = (NativeModules as { Rovq?: LegacyModule }).Rovq;
91
+ // The same failure as TurboModuleRegistry.getEnforcing on the main line.
92
+ if (module === undefined) {
93
+ throw new Error(
94
+ "ROVQ: the native module 'Rovq' was not found. Rebuild the app after installing @rovq/react-native.",
95
+ );
96
+ }
97
+ const emitter = new NativeEventEmitter(module);
98
+ return {
99
+ checkForUpdate: () => module.checkForUpdate(),
100
+ downloadUpdate: () => module.downloadUpdate(),
101
+ installUpdate: () => module.installUpdate(),
102
+ sync: () => module.sync(),
103
+ notifyApplicationReady: () => module.notifyApplicationReady(),
104
+ restartApp: () => module.restartApp(),
105
+ rollback: () => module.rollback(),
106
+ getStatus: () => module.getStatus(),
107
+ // A cast, not a type argument: older versions of NativeEventEmitter take none, and apps
108
+ // type-check this file, because the package ships TypeScript.
109
+ onDownloadProgress: (listener) =>
110
+ emitter.addListener(DOWNLOAD_PROGRESS_EVENT, (progress) =>
111
+ listener(progress as NativeDownloadProgress),
112
+ ),
113
+ };
70
114
  }
71
115
 
72
- export default TurboModuleRegistry.getEnforcing<Spec>('Rovq');
116
+ export default load();
@@ -1,26 +0,0 @@
1
- package com.rovq.engine.react
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
-
9
- /** Found by React Native autolinking. */
10
- class RovqPackage : BaseReactPackage() {
11
- override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12
- if (name == RovqModule.NAME) RovqModule(reactContext) else null
13
-
14
- override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
15
- mapOf(
16
- RovqModule.NAME to ReactModuleInfo(
17
- RovqModule.NAME,
18
- RovqModule::class.java.name,
19
- false,
20
- false,
21
- false,
22
- true,
23
- ),
24
- )
25
- }
26
- }
@@ -1,74 +0,0 @@
1
- package com.rovq.engine.react
2
-
3
- import android.app.Application
4
- import android.util.Log
5
- import com.facebook.react.ReactHost
6
- import com.facebook.react.ReactPackage
7
- import com.facebook.react.bridge.JSBundleLoader
8
- import com.facebook.react.bridge.JSBundleLoaderDelegate
9
- import com.facebook.react.common.annotations.UnstableReactNativeAPI
10
- import com.facebook.react.common.build.ReactBuildConfig
11
- import com.facebook.react.defaults.DefaultComponentsRegistry
12
- import com.facebook.react.defaults.DefaultReactHostDelegate
13
- import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
14
- import com.facebook.react.fabric.ComponentFactory
15
- import com.facebook.react.runtime.ReactHostImpl
16
- import com.facebook.react.runtime.hermes.HermesInstance
17
-
18
- private const val TAG = "Rovq"
19
- private const val EMBEDDED_BUNDLE = "assets://index.android.bundle"
20
-
21
- /**
22
- * Chooses the bundle each time JavaScript starts, not once when the host is created.
23
- * This is what lets reload apply an update without killing the process.
24
- */
25
- private class RovqBundleLoader(private val application: Application) : JSBundleLoader() {
26
- override fun loadScript(delegate: JSBundleLoaderDelegate): String {
27
- val file = try {
28
- Rovq.engine.resolveBundle()
29
- } catch (error: Throwable) {
30
- // Whatever goes wrong in the engine, the app must still start.
31
- Log.e(TAG, "Falling back to the bundle inside the app", error)
32
- null
33
- }
34
- if (file == null) {
35
- Log.i(TAG, "Loading the bundle inside the app")
36
- delegate.loadScriptFromAssets(application.assets, EMBEDDED_BUNDLE, true)
37
- return EMBEDDED_BUNDLE
38
- }
39
- Log.i(TAG, "Loading ${file.absolutePath}")
40
- delegate.loadScriptFromFile(file.absolutePath, file.absolutePath, false)
41
- return file.absolutePath
42
- }
43
- }
44
-
45
- /**
46
- * The only file that touches React Native's unstable host API. It does what
47
- * getDefaultReactHost does, with a different loader, so check it against that function
48
- * when upgrading React Native. See docs/android-bundle-loading.md.
49
- */
50
- @OptIn(UnstableReactNativeAPI::class)
51
- object RovqReactHost {
52
- /** Use in place of getDefaultReactHost in MainApplication. */
53
- fun create(application: Application, packages: List<ReactPackage>): ReactHost {
54
- val componentFactory = ComponentFactory()
55
- DefaultComponentsRegistry.register(componentFactory)
56
- val delegate = DefaultReactHostDelegate(
57
- jsMainModulePath = "index",
58
- jsBundleLoader = RovqBundleLoader(application),
59
- reactPackages = packages,
60
- jsRuntimeFactory = HermesInstance(),
61
- turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
62
- // Letting a fatal JavaScript error kill the process is what makes rollback work:
63
- // the release stays unconfirmed, so the next start abandons it.
64
- exceptionHandler = { error -> throw error },
65
- )
66
- return ReactHostImpl(
67
- application.applicationContext,
68
- delegate,
69
- componentFactory,
70
- true,
71
- ReactBuildConfig.DEBUG,
72
- )
73
- }
74
- }