@hot-updater/react-native 0.0.5 → 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.
@@ -17,6 +17,8 @@ Pod::Spec.new do |s|
17
17
  s.source_files = "ios/**/*.{h,m,mm}"
18
18
  s.public_header_files = ['ios/HotUpdater/HotUpdater.h']
19
19
 
20
+ s.dependency "SSZipArchive", "~> 2.2.2"
21
+
20
22
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
21
23
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
22
24
  if respond_to?(:install_modules_dependencies, true)
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # hot-updater (WIP)
2
2
  React Native OTA solution for internal infrastructure
3
3
 
4
- ## Usage
4
+ ## IOS Usage
5
5
  * as-is
6
6
  ```objective-c
7
7
  // filename: ios/MyApp/AppDelegate.mm
@@ -33,4 +33,64 @@ React Native OTA solution for internal infrastructure
33
33
  }
34
34
 
35
35
  // ...
36
- ```
36
+ ```
37
+
38
+ ## Android Usage
39
+ ```kotlin
40
+ package com.hotupdaterexample
41
+
42
+ import android.app.Application
43
+ import com.facebook.react.PackageList
44
+ import com.facebook.react.ReactApplication
45
+ import com.facebook.react.ReactHost
46
+ import com.facebook.react.ReactNativeHost
47
+ import com.facebook.react.ReactPackage
48
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
49
+ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
50
+ import com.facebook.react.defaults.DefaultReactNativeHost
51
+ import com.facebook.soloader.SoLoader
52
+ import com.hotupdater.HotUpdater
53
+
54
+ class MainApplication : Application(), ReactApplication {
55
+
56
+ override val reactNativeHost: ReactNativeHost =
57
+ object : DefaultReactNativeHost(this) {
58
+ override fun getPackages(): List<ReactPackage> =
59
+ PackageList(this).packages.apply {
60
+ // Packages that cannot be autolinked yet can be added manually here, for example:
61
+ // add(MyReactNativePackage())
62
+ }
63
+
64
+ override fun getJSMainModuleName(): String = "index"
65
+
66
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
67
+
68
+ override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
69
+ override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
70
+
71
+ override fun getJSBundleFile(): String? {
72
+ // This field
73
+ return HotUpdater.getJSBundleFile() ?: super.getJSBundleFile()
74
+ }
75
+ }
76
+
77
+ override val reactHost: ReactHost
78
+ get() = getDefaultReactHost(applicationContext, reactNativeHost)
79
+
80
+ override fun onCreate() {
81
+ super.onCreate()
82
+ SoLoader.init(this, false)
83
+ // This field
84
+ HotUpdater.init(applicationContext, reactNativeHost)
85
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
86
+ // If you opted-in for the New Architecture, we load the native entry point for this app.
87
+ load()
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Android Debug
94
+ ```sh
95
+ > adb logcat -s HotUpdater
96
+ ```
@@ -0,0 +1,120 @@
1
+ buildscript {
2
+ // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["HotUpdater_kotlinVersion"]
4
+
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+
10
+ dependencies {
11
+ classpath "com.android.tools.build:gradle:7.2.1"
12
+ // noinspection DifferentKotlinGradleVersion
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
+ }
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: "com.android.library"
22
+ apply plugin: "kotlin-android"
23
+
24
+ if (isNewArchitectureEnabled()) {
25
+ apply plugin: "com.facebook.react"
26
+ }
27
+
28
+ def getExtOrDefault(name) {
29
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["HotUpdater_" + name]
30
+ }
31
+
32
+ def getExtOrIntegerDefault(name) {
33
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["HotUpdater_" + name]).toInteger()
34
+ }
35
+
36
+ def supportsNamespace() {
37
+ def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
38
+ def major = parsed[0].toInteger()
39
+ def minor = parsed[1].toInteger()
40
+
41
+ // Namespace support was added in 7.3.0
42
+ return (major == 7 && minor >= 3) || major >= 8
43
+ }
44
+
45
+ android {
46
+ if (supportsNamespace()) {
47
+ namespace "com.hotupdater"
48
+
49
+ sourceSets {
50
+ main {
51
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
52
+ }
53
+ }
54
+ }
55
+
56
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
57
+
58
+ defaultConfig {
59
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
60
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
61
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
62
+
63
+ }
64
+
65
+ buildFeatures {
66
+ buildConfig true
67
+ }
68
+
69
+ buildTypes {
70
+ release {
71
+ minifyEnabled false
72
+ }
73
+ }
74
+
75
+ lintOptions {
76
+ disable "GradleCompatible"
77
+ }
78
+
79
+ compileOptions {
80
+ sourceCompatibility JavaVersion.VERSION_1_8
81
+ targetCompatibility JavaVersion.VERSION_1_8
82
+ }
83
+
84
+ sourceSets {
85
+ main {
86
+ if (isNewArchitectureEnabled()) {
87
+ java.srcDirs += [
88
+ "src/newarch",
89
+ // This is needed to build Kotlin project with NewArch enabled
90
+ "${project.buildDir}/generated/source/codegen/java"
91
+ ]
92
+ } else {
93
+ java.srcDirs += ["src/oldarch"]
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ repositories {
100
+ mavenCentral()
101
+ google()
102
+ }
103
+
104
+ def kotlin_version = getExtOrDefault("kotlinVersion")
105
+
106
+ dependencies {
107
+ // For < 0.71, this will be from the local maven repo
108
+ // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
109
+ //noinspection GradleDynamicVersion
110
+ implementation "com.facebook.react:react-native:+"
111
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
112
+ }
113
+
114
+ if (isNewArchitectureEnabled()) {
115
+ react {
116
+ jsRootDir = file("../src/")
117
+ libraryName = "HotUpdater"
118
+ codegenJavaPackageName = "com.hotupdater"
119
+ }
120
+ }
@@ -0,0 +1,5 @@
1
+ HotUpdater_kotlinVersion=1.7.0
2
+ HotUpdater_minSdkVersion=21
3
+ HotUpdater_targetSdkVersion=31
4
+ HotUpdater_compileSdkVersion=31
5
+ HotUpdater_ndkversion=21.4.7075529
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.hotupdater">
3
+ </manifest>
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,290 @@
1
+ package com.hotupdater
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.util.Log
8
+ import com.facebook.react.ReactInstanceManager
9
+ import com.facebook.react.ReactNativeHost
10
+ import com.facebook.react.bridge.JSBundleLoader
11
+ import com.facebook.react.bridge.LifecycleEventListener
12
+ import java.io.File
13
+ import java.lang.reflect.Field
14
+ import java.net.URL
15
+ import java.util.zip.ZipFile
16
+
17
+ class HotUpdater internal constructor(context: Context, reactNativeHost: ReactNativeHost) {
18
+ private val mContext: Context = context
19
+ private val mReactNativeHost: ReactNativeHost = reactNativeHost
20
+
21
+ companion object {
22
+ private var mCurrentInstance: HotUpdater? = null
23
+
24
+ fun init(context: Context, reactNativeHost: ReactNativeHost): HotUpdater {
25
+ Log.d("HotUpdater", "Initializing HotUpdater")
26
+
27
+ return mCurrentInstance
28
+ ?: synchronized(this) {
29
+ mCurrentInstance
30
+ ?: HotUpdater(context, reactNativeHost).also {
31
+ mCurrentInstance = it
32
+ }
33
+ }
34
+ }
35
+
36
+ fun getAppVersion(): String? {
37
+ return mCurrentInstance?.getAppVersion()
38
+ }
39
+
40
+ fun initializeOnAppUpdate() {
41
+ mCurrentInstance?.initializeOnAppUpdate()
42
+ }
43
+
44
+ fun reload() {
45
+ mCurrentInstance?.reload()
46
+ }
47
+
48
+ fun getJSBundleFile(): String? {
49
+ Log.d("HotUpdater", "Getting JS bundle file ${mCurrentInstance?.getBundleURL()}")
50
+ return mCurrentInstance?.getBundleURL()
51
+ }
52
+
53
+ fun getBundleVersion(): Double? {
54
+ return mCurrentInstance?.getBundleVersion()
55
+ }
56
+
57
+ fun updateBundle(prefix: String, url: String?): Boolean? {
58
+ return mCurrentInstance?.updateBundle(prefix, url)
59
+ }
60
+ }
61
+
62
+ private val documentsDir: String
63
+ get() = mContext.getExternalFilesDir(null)?.absolutePath ?: mContext.filesDir.absolutePath
64
+
65
+ private fun convertFileSystemPathFromBasePath(basePath: String): String {
66
+ val separator = if (basePath.startsWith("/")) "" else "/"
67
+ return "$documentsDir$separator$basePath"
68
+ }
69
+
70
+ private fun stripPrefixFromPath(prefix: String, path: String): String {
71
+ return if (path.startsWith("/$prefix/")) {
72
+ path.replaceFirst("/$prefix/", "")
73
+ } else {
74
+ path
75
+ }
76
+ }
77
+
78
+ private fun loadBundleLegacy() {
79
+ val currentActivity: Activity? =
80
+ mReactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
81
+ if (currentActivity == null) {
82
+ return
83
+ }
84
+
85
+ currentActivity.runOnUiThread { currentActivity.recreate() }
86
+ }
87
+ private var mLifecycleEventListener: LifecycleEventListener? = null
88
+
89
+ private fun clearLifecycleEventListener() {
90
+ if (mLifecycleEventListener != null) {
91
+ mReactNativeHost.reactInstanceManager.currentReactContext?.removeLifecycleEventListener(
92
+ mLifecycleEventListener
93
+ )
94
+ mLifecycleEventListener = null
95
+ }
96
+ }
97
+
98
+ private fun setJSBundle(instanceManager: ReactInstanceManager, latestJSBundleFile: String?) {
99
+
100
+ try {
101
+ var latestJSBundleLoader: JSBundleLoader? = null
102
+
103
+ if (latestJSBundleFile != null && latestJSBundleFile.lowercase().startsWith("assets://")
104
+ ) {
105
+ latestJSBundleLoader =
106
+ JSBundleLoader.createAssetLoader(
107
+ instanceManager.currentReactContext,
108
+ latestJSBundleFile,
109
+ false
110
+ )
111
+ } else if (latestJSBundleFile != null) {
112
+ latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile)
113
+ }
114
+ val bundleLoaderField: Field =
115
+ instanceManager::class.java.getDeclaredField("mBundleLoader")
116
+ bundleLoaderField.isAccessible = true
117
+
118
+ if (latestJSBundleLoader != null) {
119
+ bundleLoaderField.set(instanceManager, latestJSBundleLoader)
120
+ } else {
121
+ bundleLoaderField.set(instanceManager, null)
122
+ }
123
+ } catch (e: Exception) {
124
+ throw IllegalAccessException("Could not setJSBundle")
125
+ }
126
+ }
127
+
128
+ fun initializeOnAppUpdate() {
129
+ val sharedPreferences =
130
+ mContext.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
131
+
132
+ val currentVersion = getAppVersion()
133
+ val savedVersion = sharedPreferences.getString("HotUpdaterAppVersion", null)
134
+
135
+ if (currentVersion != savedVersion) {
136
+ val editor = sharedPreferences.edit()
137
+ editor.remove("HotUpdaterBundleURL")
138
+ editor.remove("HotUpdaterBundleVersion")
139
+ editor.putString("HotUpdaterAppVersion", currentVersion)
140
+ editor.apply()
141
+ }
142
+ }
143
+
144
+ fun reload() {
145
+ Log.d("HotUpdater", "HotUpdater requested a reload ${getBundleURL()}")
146
+
147
+ setJSBundle(mReactNativeHost.reactInstanceManager, getBundleURL())
148
+
149
+ clearLifecycleEventListener()
150
+ try {
151
+ Handler(Looper.getMainLooper()).post {
152
+ try {
153
+ mReactNativeHost.reactInstanceManager.recreateReactContextInBackground()
154
+ } catch (t: Throwable) {
155
+ loadBundleLegacy()
156
+ }
157
+ }
158
+ } catch (t: Throwable) {
159
+ loadBundleLegacy()
160
+ }
161
+ }
162
+
163
+ fun getAppVersion(): String {
164
+ val packageInfo = mContext.packageManager.getPackageInfo(mContext.packageName, 0)
165
+ return packageInfo.versionName
166
+ }
167
+
168
+ fun getBundleURL(): String {
169
+ val sharedPreferences =
170
+ mContext.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
171
+ val urlString = sharedPreferences.getString("HotUpdaterBundleURL", null)
172
+ if (urlString.isNullOrEmpty()) {
173
+ return "assets://index.android.bundle"
174
+ }
175
+
176
+ return urlString
177
+ }
178
+
179
+ private fun setBundleURL(bundleURL: String?) {
180
+ val sharedPreferences =
181
+ mContext.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
182
+ with(sharedPreferences.edit()) {
183
+ putString("HotUpdaterBundleURL", bundleURL)
184
+ apply()
185
+ }
186
+ }
187
+ private fun setBundleVersion(bundleVersion: String?) {
188
+ val sharedPreferences =
189
+ mContext.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
190
+ with(sharedPreferences.edit()) {
191
+ putString("HotUpdaterBundleVersion", bundleVersion)
192
+ apply()
193
+ }
194
+ }
195
+
196
+ fun getBundleVersion(): Double? {
197
+ val sharedPreferences =
198
+ mContext.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
199
+ val bundleVersion = sharedPreferences.getString("HotUpdaterBundleVersion", null)
200
+ Log.d("HotUpdater", "Bundle version: $bundleVersion")
201
+ return if (bundleVersion != null && bundleVersion.isNotEmpty()) {
202
+ try {
203
+ bundleVersion.toDouble()
204
+ } catch (e: Exception) {
205
+ -1.0
206
+ }
207
+ } else {
208
+ -1.0
209
+ }
210
+ }
211
+
212
+ private fun extractZipFileAtPath(filePath: String, destinationPath: String): Boolean {
213
+ return try {
214
+ ZipFile(filePath).use { zip ->
215
+ zip.entries().asSequence().forEach { entry ->
216
+ val file = File(destinationPath, entry.name)
217
+ if (entry.isDirectory) {
218
+ file.mkdirs()
219
+ } else {
220
+ file.parentFile?.mkdirs()
221
+ zip.getInputStream(entry).use { input ->
222
+ file.outputStream().use { output -> input.copyTo(output) }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ true
228
+ } catch (e: Exception) {
229
+ Log.d("HotUpdater", "Failed to unzip file: ${e.message}")
230
+ false
231
+ }
232
+ }
233
+
234
+ fun updateBundle(prefix: String, url: String?): Boolean {
235
+ if (url == null) {
236
+ setBundleURL(null)
237
+ setBundleVersion(null)
238
+ return true
239
+ }
240
+
241
+ val downloadUrl = URL(url)
242
+
243
+ val basePath = stripPrefixFromPath(prefix, downloadUrl.path)
244
+ val path = convertFileSystemPathFromBasePath(basePath)
245
+
246
+ val data =
247
+ try {
248
+ downloadUrl.readBytes()
249
+ } catch (e: Exception) {
250
+ Log.d("HotUpdater", "Failed to download data from URL: $url")
251
+ return false
252
+ }
253
+
254
+ val file = File(path)
255
+ try {
256
+ file.parentFile?.mkdirs()
257
+ file.writeBytes(data)
258
+ } catch (e: Exception) {
259
+ Log.d("HotUpdater", "Failed to save data: ${e.message}")
260
+ return false
261
+ }
262
+
263
+ val extractedPath = file.parentFile?.path
264
+ if (extractedPath == null) {
265
+ return false
266
+ }
267
+
268
+ if (!extractZipFileAtPath(path, extractedPath)) {
269
+ Log.d("HotUpdater", "Failed to extract zip file.")
270
+ return false
271
+ }
272
+
273
+ val extractedDirectory = File(extractedPath)
274
+ val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle.js" }
275
+
276
+ if (indexFile != null) {
277
+ val bundlePath = indexFile.path
278
+ Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
279
+ setBundleURL(bundlePath)
280
+ } else {
281
+ Log.d("HotUpdater", "index.android.bundle.js not found.")
282
+ return false
283
+ }
284
+
285
+ setBundleVersion(prefix)
286
+ Log.d("HotUpdater", "Downloaded and extracted file successfully.")
287
+
288
+ return true
289
+ }
290
+ }
@@ -0,0 +1,45 @@
1
+ package com.hotupdater
2
+
3
+ import com.facebook.react.bridge.Callback
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactMethod
6
+
7
+ class HotUpdaterModule internal constructor(context: ReactApplicationContext) :
8
+ HotUpdaterSpec(context) {
9
+
10
+ private val mReactApplicationContext: ReactApplicationContext = context
11
+
12
+ override fun getName(): String {
13
+ return NAME
14
+ }
15
+
16
+ @ReactMethod
17
+ override fun initializeOnAppUpdate() {
18
+ HotUpdater.initializeOnAppUpdate()
19
+ }
20
+
21
+ @ReactMethod
22
+ override fun reload() {
23
+ HotUpdater.reload()
24
+ }
25
+
26
+ @ReactMethod
27
+ override fun getAppVersion(callback: Callback) {
28
+ callback.invoke(HotUpdater.getAppVersion())
29
+ }
30
+
31
+ @ReactMethod
32
+ override fun getBundleVersion(callback: Callback) {
33
+ callback.invoke(HotUpdater.getBundleVersion())
34
+ }
35
+
36
+ @ReactMethod
37
+ override fun updateBundle(prefix: String, url: String?, callback: Callback) {
38
+ val result = HotUpdater.updateBundle(prefix, url)
39
+ callback.invoke(result)
40
+ }
41
+
42
+ companion object {
43
+ const val NAME = "HotUpdater"
44
+ }
45
+ }
@@ -0,0 +1,36 @@
1
+ package com.hotupdater
2
+
3
+ import com.facebook.react.TurboReactPackage
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 HotUpdaterPackage : TurboReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == HotUpdaterModule.NAME) {
13
+ HotUpdaterModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ val isTurboModule: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
23
+ moduleInfos[HotUpdaterModule.NAME] =
24
+ ReactModuleInfo(
25
+ HotUpdaterModule.NAME,
26
+ HotUpdaterModule.NAME,
27
+ false, // canOverrideExistingModule
28
+ false, // needsEagerInit
29
+ true, // hasConstants
30
+ false, // isCxxModule
31
+ isTurboModule // isTurboModule
32
+ )
33
+ moduleInfos
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,6 @@
1
+ package com.hotupdater
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+
5
+ abstract class HotUpdaterSpec internal constructor(context: ReactApplicationContext) :
6
+ NativeHotUpdaterSpec(context) {}
@@ -0,0 +1,15 @@
1
+ package com.hotupdater
2
+
3
+ import com.facebook.react.bridge.Callback
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+
7
+ abstract class HotUpdaterSpec internal constructor(context: ReactApplicationContext) :
8
+ ReactContextBaseJavaModule(context) {
9
+
10
+ abstract fun updateBundle(prefix: String, url: String?, callback: Callback)
11
+ abstract fun reload()
12
+ abstract fun initializeOnAppUpdate()
13
+ abstract fun getAppVersion(callback: Callback)
14
+ abstract fun getBundleVersion(callback: Callback)
15
+ }