react-native-ota-hot-update 1.0.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 +21 -0
- package/README.md +102 -0
- package/android/build.gradle +99 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/rnhotupdate/HotUpdateModule.java +142 -0
- package/android/src/main/java/com/rnhotupdate/OtaHotUpdate.java +45 -0
- package/android/src/main/java/com/rnhotupdate/SharedPrefs.kt +31 -0
- package/ios/RNhotupdate.h +12 -0
- package/ios/RNhotupdate.mm +160 -0
- package/package.json +27 -0
- package/react-native.config.js +9 -0
- package/rn-hotupdate.podspec +41 -0
- package/src/Utils.ts +14 -0
- package/src/index.tsx +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Avishay Bar
|
|
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
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# react-native-ota-hot-update
|
|
2
|
+
|
|
3
|
+
A React Native module that allows you to control hot update same as Code Push, you can control version manager, hosting bundle js by your self, this library just control install the hot update after bundle js downloaded from your side. As we know, Code push is going to retirement in next year, that why i create that library for you can control bundle js from your backend side.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
iOS GIF | Android GIF
|
|
7
|
+
:-------------------------:|:-------------------------:
|
|
8
|
+
<img src="./ioshotupdate.gif" title="iOS GIF" width="250"> | <img src="./androidhotupdate.gif" title="Android GIF" width="250">
|
|
9
|
+
|
|
10
|
+
[](https://img.shields.io/npm/dw/react-native-ota-hot-update)
|
|
11
|
+
[](https://img.shields.io/npm/v/react-native-ota-hot-update?color=red)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
if you don't want to manage the download progress, need to install blob util together:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add react-native-ota-hot-update && react-native-blob-util
|
|
19
|
+
```
|
|
20
|
+
Auto linking already, need pod install for ios:
|
|
21
|
+
```bash
|
|
22
|
+
cd ios && pod install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## That's it, can check the example code
|
|
26
|
+
|
|
27
|
+
Here is the guideline to control bundle js by yourself, in here i am using Firebase storage to store bundlejs file and a json file that announce new version is comming:
|
|
28
|
+
|
|
29
|
+
#### 1.Add these script into your package.json to export bundlejs file:
|
|
30
|
+
```bash
|
|
31
|
+
"scripts": {
|
|
32
|
+
"export-android": "mkdir -p android/output && react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/output/index.android.bundle --assets-dest android/app/src/main/res && zip -j android/output/index.android.bundle.zip android/output/index.android.bundle && rm -rf android/output/index.android.bundle",
|
|
33
|
+
"export-ios": "mkdir -p ios/output && react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/output/main.jsbundle && zip -j ios/output/main.jsbundle.zip ios/output/main.jsbundle && rm -rf ios/output/main.jsbundle"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
These commands are export bundle file and compress it as a zip file, one for android and one for ios. You can create your own script that export and auto upload to your server.
|
|
37
|
+
|
|
38
|
+
Then create an json file: `update.json` like that:
|
|
39
|
+
```bash
|
|
40
|
+
{
|
|
41
|
+
"version": 1,
|
|
42
|
+
"downloadAndroidUrl": "https://firebasestorage.googleapis.com/v0/b/ota-demo-68f38.appspot.com/o/index.android.bundle.zip?alt=media",
|
|
43
|
+
"downloadIosUrl": "https://firebasestorage.googleapis.com/v0/b/ota-demo-68f38.appspot.com/o/main.jsbundle.zip?alt=media"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then upload your bundlejs files to firebase storage, totally will look like that:
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
After you have done everything related to version manager, you just handle the way to update new version like fetch update.json as api to get download url and call this function:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
import hotUpdate from 'react-native-ota-hot-update';
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
hotUpdate.downloadBundleUri(url, version, {
|
|
58
|
+
updateSuccess: () => {
|
|
59
|
+
console.log('update success!');
|
|
60
|
+
},
|
|
61
|
+
updateFail(message?: string) {
|
|
62
|
+
Alert.alert('Update failed!', message, [
|
|
63
|
+
{
|
|
64
|
+
text: 'Cancel',
|
|
65
|
+
onPress: () => console.log('Cancel Pressed'),
|
|
66
|
+
style: 'cancel',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
},
|
|
70
|
+
restartAfterInstall: true,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The important thing: this library will control `version` by it self, need always pass version as parameter in `downloadBundleUri`, it will storage as a cache and use this to check whether need update version in the next time. Default of `version` is **0**
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## Functions
|
|
78
|
+
|
|
79
|
+
| key | Return | Action | Parameters |
|
|
80
|
+
| ------------ |--------|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
|
81
|
+
| downloadBundleUri | void | Download bundle and install it | (uri: string, version: number, option?: **UpdateOption**) |
|
|
82
|
+
| setupBundlePath | boolean | Install your bundle path if you control the downloading by your self, ignore that if you use `downloadBundleUri` | path: string, the path of bundlejs file that you downloaded before |
|
|
83
|
+
| removeUpdate | void | Remove you update and use the previos version | restartAfterRemoved?: boolean, restart to apply your changing |
|
|
84
|
+
| resetApp | void | Restart the app to apply the changing | empty |
|
|
85
|
+
| getCurrentVersion | number | Get the current version that let you use to compare and control the logic updating | empty |
|
|
86
|
+
| setCurrentVersion | boolean | Set the current version that let you use to compare and control the logic updating | version: number |
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## UpdateOption
|
|
90
|
+
|
|
91
|
+
| Option | Required | Type | Description |
|
|
92
|
+
|-------------------------|----------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
93
|
+
| headers | No | Object | The header to down load your uri bundle file if need token/authentication... |
|
|
94
|
+
| updateSuccess | No | Callback | Will trigger when install update success |
|
|
95
|
+
| updateFail(message: string) | No | Callback | Will trigger when install update failed |
|
|
96
|
+
| restartAfterInstall | No | boolean | default is `false`, if `true` will restart the app after install success to apply the new change |
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](LICENSE.md)
|
|
@@ -0,0 +1,99 @@
|
|
|
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["RNhotupdate_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 reactNativeArchitectures() {
|
|
18
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
19
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def isNewArchitectureEnabled() {
|
|
23
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
apply plugin: "com.android.library"
|
|
27
|
+
apply plugin: "kotlin-android"
|
|
28
|
+
|
|
29
|
+
if (isNewArchitectureEnabled()) {
|
|
30
|
+
apply plugin: "com.facebook.react"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def getExtOrDefault(name) {
|
|
34
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["RNhotupdate_" + name]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def getExtOrIntegerDefault(name) {
|
|
38
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["RNhotupdate_" + name]).toInteger()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def supportsNamespace() {
|
|
42
|
+
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
|
|
43
|
+
def major = parsed[0].toInteger()
|
|
44
|
+
def minor = parsed[1].toInteger()
|
|
45
|
+
|
|
46
|
+
// Namespace support was added in 7.3.0
|
|
47
|
+
return (major == 7 && minor >= 3) || major >= 8
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
android {
|
|
51
|
+
if (supportsNamespace()) {
|
|
52
|
+
namespace "com.rnhotupdate"
|
|
53
|
+
|
|
54
|
+
sourceSets {
|
|
55
|
+
main {
|
|
56
|
+
manifest.srcFile "src/main/AndroidManifestNew.xml"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
62
|
+
|
|
63
|
+
defaultConfig {
|
|
64
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
65
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
66
|
+
|
|
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
|
+
|
|
85
|
+
repositories {
|
|
86
|
+
mavenCentral()
|
|
87
|
+
google()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
91
|
+
|
|
92
|
+
dependencies {
|
|
93
|
+
// For < 0.71, this will be from the local maven repo
|
|
94
|
+
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
95
|
+
//noinspection GradleDynamicVersion
|
|
96
|
+
implementation "com.facebook.react:react-native:+"
|
|
97
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
package com.rnhotupdate;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.Intent;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
|
|
7
|
+
import com.facebook.react.ReactInstanceManager;
|
|
8
|
+
import com.facebook.react.bridge.Promise;
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
10
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
11
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
12
|
+
import android.os.Process;
|
|
13
|
+
import java.io.File;
|
|
14
|
+
import java.io.FileInputStream;
|
|
15
|
+
import java.io.FileOutputStream;
|
|
16
|
+
import java.io.InputStream;
|
|
17
|
+
import java.io.OutputStream;
|
|
18
|
+
import java.util.zip.ZipEntry;
|
|
19
|
+
import java.util.zip.ZipInputStream;
|
|
20
|
+
import androidx.annotation.NonNull;
|
|
21
|
+
public class HotUpdateModule extends ReactContextBaseJavaModule {
|
|
22
|
+
public HotUpdateModule(ReactApplicationContext reactContext) {
|
|
23
|
+
super(reactContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private boolean deleteOldBundleIfneeded() {
|
|
27
|
+
SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
|
|
28
|
+
String path = sharedPrefs.getString(Common.INSTANCE.getPATH());
|
|
29
|
+
File file = new File(path);
|
|
30
|
+
if (file.exists() && file.isFile()) {
|
|
31
|
+
boolean isDeleted = file.delete();
|
|
32
|
+
sharedPrefs.clear();
|
|
33
|
+
return isDeleted;
|
|
34
|
+
} else {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
private String unzip(File zipFile) {
|
|
39
|
+
File destDir = zipFile.getParentFile(); // Directory of the zip file
|
|
40
|
+
|
|
41
|
+
String filePathExtracted = null;
|
|
42
|
+
if (!destDir.exists()) {
|
|
43
|
+
destDir.mkdirs();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try (InputStream is = new FileInputStream(zipFile);
|
|
47
|
+
ZipInputStream zis = new ZipInputStream(is)) {
|
|
48
|
+
|
|
49
|
+
ZipEntry zipEntry;
|
|
50
|
+
while ((zipEntry = zis.getNextEntry()) != null) {
|
|
51
|
+
File newFile = new File(destDir, zipEntry.getName());
|
|
52
|
+
if (zipEntry.isDirectory()) {
|
|
53
|
+
newFile.mkdirs();
|
|
54
|
+
} else {
|
|
55
|
+
// Create directories if they do not exist
|
|
56
|
+
new File(newFile.getParent()).mkdirs();
|
|
57
|
+
// Extract the file
|
|
58
|
+
try (OutputStream os = new FileOutputStream(newFile)) {
|
|
59
|
+
byte[] buffer = new byte[1024];
|
|
60
|
+
int len;
|
|
61
|
+
while ((len = zis.read(buffer)) > 0) {
|
|
62
|
+
os.write(buffer, 0, len);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
filePathExtracted = newFile.getAbsolutePath();
|
|
67
|
+
zis.closeEntry();
|
|
68
|
+
}
|
|
69
|
+
} catch (Exception e) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return filePathExtracted;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactMethod
|
|
76
|
+
public void setupBundlePath(String path, Promise promise) {
|
|
77
|
+
if (path != null) {
|
|
78
|
+
File file = new File(path);
|
|
79
|
+
if (file.exists() && file.isFile()) {
|
|
80
|
+
String fileUnzip = unzip(file);
|
|
81
|
+
Log.d("setupBundlePath: ", fileUnzip);
|
|
82
|
+
if (fileUnzip != null) {
|
|
83
|
+
file.delete();
|
|
84
|
+
deleteOldBundleIfneeded();
|
|
85
|
+
SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
|
|
86
|
+
sharedPrefs.putString(Common.INSTANCE.getPATH(), fileUnzip);
|
|
87
|
+
promise.resolve(true);
|
|
88
|
+
} else {
|
|
89
|
+
promise.resolve(false);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
promise.resolve(false);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
promise.resolve(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@ReactMethod
|
|
100
|
+
public void deleteBundle(Promise promise) {
|
|
101
|
+
boolean isDeleted = deleteOldBundleIfneeded();
|
|
102
|
+
promise.resolve(isDeleted);
|
|
103
|
+
}
|
|
104
|
+
@ReactMethod
|
|
105
|
+
public void restart() {
|
|
106
|
+
Context context = getCurrentActivity();
|
|
107
|
+
Intent intent = context.getPackageManager()
|
|
108
|
+
.getLaunchIntentForPackage(context.getPackageName());
|
|
109
|
+
if (intent != null) {
|
|
110
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
|
|
111
|
+
Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
112
|
+
Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
113
|
+
|
|
114
|
+
context.startActivity(intent);
|
|
115
|
+
Process.killProcess(Process.myPid());
|
|
116
|
+
System.exit(0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
@ReactMethod
|
|
120
|
+
public void getCurrentVersion(Promise promise) {
|
|
121
|
+
SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
|
|
122
|
+
String version = sharedPrefs.getString(Common.INSTANCE.getVERSION());
|
|
123
|
+
if (!version.equals("")) {
|
|
124
|
+
promise.resolve(version);
|
|
125
|
+
} else {
|
|
126
|
+
promise.resolve("0");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
@ReactMethod
|
|
131
|
+
public void setCurrentVersion(String version, Promise promise) {
|
|
132
|
+
SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
|
|
133
|
+
sharedPrefs.putString(Common.INSTANCE.getVERSION(), version);
|
|
134
|
+
promise.resolve(true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@NonNull
|
|
138
|
+
@Override
|
|
139
|
+
public String getName() {
|
|
140
|
+
return "RNhotupdate";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.rnhotupdate;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
import androidx.annotation.NonNull;
|
|
15
|
+
|
|
16
|
+
public class OtaHotUpdate implements ReactPackage {
|
|
17
|
+
private static Context mContext;
|
|
18
|
+
public OtaHotUpdate(Context context) {
|
|
19
|
+
mContext = context;
|
|
20
|
+
}
|
|
21
|
+
@NonNull
|
|
22
|
+
@Override
|
|
23
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
|
24
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
25
|
+
modules.add(new HotUpdateModule(reactApplicationContext));
|
|
26
|
+
return modules;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@NonNull
|
|
30
|
+
@Override
|
|
31
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
|
32
|
+
return Collections.emptyList();
|
|
33
|
+
}
|
|
34
|
+
public static String getBundleJS() {
|
|
35
|
+
if (mContext == null) {
|
|
36
|
+
return Common.INSTANCE.getDEFAULT_BUNDLE();
|
|
37
|
+
}
|
|
38
|
+
SharedPrefs sharedPrefs = new SharedPrefs(mContext);
|
|
39
|
+
String pathBundle = sharedPrefs.getString(Common.INSTANCE.getPATH());
|
|
40
|
+
if (pathBundle.equals("")) {
|
|
41
|
+
return Common.INSTANCE.getDEFAULT_BUNDLE();
|
|
42
|
+
}
|
|
43
|
+
return pathBundle;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.rnhotupdate
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.SharedPreferences
|
|
6
|
+
|
|
7
|
+
class SharedPrefs internal constructor(context: Context) {
|
|
8
|
+
private val mSharedPreferences: SharedPreferences =
|
|
9
|
+
context.getSharedPreferences(Common.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)
|
|
10
|
+
|
|
11
|
+
fun getString(key: String?): String? {
|
|
12
|
+
return mSharedPreferences.getString(key, "")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@SuppressLint("CommitPrefEdits")
|
|
16
|
+
fun putString(key: String?, value: String?) {
|
|
17
|
+
val editor = mSharedPreferences.edit()
|
|
18
|
+
editor.putString(key, value)
|
|
19
|
+
editor.apply()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fun clear() {
|
|
23
|
+
mSharedPreferences.edit().clear().apply()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
object Common {
|
|
27
|
+
val PATH = "PATH"
|
|
28
|
+
val VERSION = "VERSION"
|
|
29
|
+
val SHARED_PREFERENCE_NAME = "HOT-UPDATE-REACT_NATIVE"
|
|
30
|
+
val DEFAULT_BUNDLE = "assets://index.android.bundle"
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <React/RCTReloadCommand.h>
|
|
2
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
3
|
+
#import "RNRNhotupdateSpec.h"
|
|
4
|
+
|
|
5
|
+
@interface RNhotupdate : NSObject <NativeRNhotupdateSpec>
|
|
6
|
+
#else
|
|
7
|
+
#import <React/RCTBridgeModule.h>
|
|
8
|
+
|
|
9
|
+
@interface RNhotupdate : NSObject <RCTBridgeModule>
|
|
10
|
+
#endif
|
|
11
|
+
+ (NSURL *)getBundle;
|
|
12
|
+
@end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#import "RNhotupdate.h"
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
#import <SSZipArchive/SSZipArchive.h>
|
|
4
|
+
|
|
5
|
+
@implementation RNhotupdate
|
|
6
|
+
RCT_EXPORT_MODULE()
|
|
7
|
+
|
|
8
|
+
// Check if a file path is valid
|
|
9
|
+
- (BOOL)isFilePathValid:(NSString *)path {
|
|
10
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
11
|
+
return [fileManager fileExistsAtPath:path];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
- (BOOL)removeBundleIfNeeded {
|
|
15
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
16
|
+
NSString *retrievedString = [defaults stringForKey:@"PATH"];
|
|
17
|
+
if (retrievedString && [self isFilePathValid:retrievedString]) {
|
|
18
|
+
BOOL isDeleted = [self deleteFileAtPath:retrievedString];
|
|
19
|
+
[defaults removeObjectForKey:@"PATH"];
|
|
20
|
+
[defaults synchronize];
|
|
21
|
+
return isDeleted;
|
|
22
|
+
} else {
|
|
23
|
+
return NO;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Delete a file at the specified path
|
|
28
|
+
- (BOOL)deleteFileAtPath:(NSString *)path {
|
|
29
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
30
|
+
NSError *error = nil;
|
|
31
|
+
BOOL success = [fileManager removeItemAtPath:path error:&error];
|
|
32
|
+
if (!success) {
|
|
33
|
+
RCTLogError(@"Error deleting file: %@", [error localizedDescription]);
|
|
34
|
+
}
|
|
35
|
+
return success;
|
|
36
|
+
}
|
|
37
|
+
+ (BOOL)isFilePathExist:(NSString *)path {
|
|
38
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
39
|
+
return [fileManager fileExistsAtPath:path];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
+ (NSURL *)getBundle {
|
|
43
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
44
|
+
NSString *retrievedString = [defaults stringForKey:@"PATH"];
|
|
45
|
+
if (retrievedString && [self isFilePathExist:retrievedString]) {
|
|
46
|
+
NSURL *fileURL = [NSURL fileURLWithPath:retrievedString];
|
|
47
|
+
return fileURL;
|
|
48
|
+
} else {
|
|
49
|
+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (NSString *)unzipFileAtPath:(NSString *)zipFilePath {
|
|
54
|
+
// Define the directory where the files will be extracted
|
|
55
|
+
NSString *extractedFolderPath = [[zipFilePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"unzip"];
|
|
56
|
+
|
|
57
|
+
// Create the directory if it does not exist
|
|
58
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
59
|
+
if (![fileManager fileExistsAtPath:extractedFolderPath]) {
|
|
60
|
+
NSError *error = nil;
|
|
61
|
+
[fileManager createDirectoryAtPath:extractedFolderPath withIntermediateDirectories:YES attributes:nil error:&error];
|
|
62
|
+
if (error) {
|
|
63
|
+
[self deleteFileAtPath:zipFilePath];
|
|
64
|
+
NSLog(@"Failed to create directory: %@", error.localizedDescription);
|
|
65
|
+
return nil;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Unzip the file
|
|
70
|
+
BOOL success = [SSZipArchive unzipFileAtPath:zipFilePath toDestination:extractedFolderPath];
|
|
71
|
+
if (!success) {
|
|
72
|
+
[self deleteFileAtPath:zipFilePath];
|
|
73
|
+
NSLog(@"Failed to unzip file");
|
|
74
|
+
return nil;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Find the extracted file (assuming only one file in the zip)
|
|
78
|
+
NSArray *contents = [fileManager contentsOfDirectoryAtPath:extractedFolderPath error:nil];
|
|
79
|
+
if (contents.count == 1) {
|
|
80
|
+
NSString *filePath = [extractedFolderPath stringByAppendingPathComponent:contents.firstObject];
|
|
81
|
+
|
|
82
|
+
// Delete the zip file after extraction
|
|
83
|
+
NSError *removeError = nil;
|
|
84
|
+
[fileManager removeItemAtPath:zipFilePath error:&removeError];
|
|
85
|
+
if (removeError) {
|
|
86
|
+
NSLog(@"Failed to delete zip file: %@", removeError.localizedDescription);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Return the exact file path
|
|
90
|
+
return filePath;
|
|
91
|
+
} else {
|
|
92
|
+
[self deleteFileAtPath:zipFilePath];
|
|
93
|
+
NSLog(@"Expected one file in the zip but found %lu", (unsigned long)contents.count);
|
|
94
|
+
return nil;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Expose setupBundlePath method to JavaScript
|
|
99
|
+
RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) {
|
|
100
|
+
if ([self isFilePathValid:path]) {
|
|
101
|
+
//Unzip file
|
|
102
|
+
NSString *extractedFilePath = [self unzipFileAtPath:path];
|
|
103
|
+
NSLog(@"file extraction----- %@", extractedFilePath);
|
|
104
|
+
if (extractedFilePath) {
|
|
105
|
+
[self removeBundleIfNeeded];
|
|
106
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
107
|
+
[defaults setObject:extractedFilePath forKey:@"PATH"];
|
|
108
|
+
[defaults synchronize];
|
|
109
|
+
resolve(@(YES));
|
|
110
|
+
} else {
|
|
111
|
+
resolve(@(NO));
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
resolve(@(NO));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Expose deleteBundle method to JavaScript
|
|
119
|
+
RCT_EXPORT_METHOD(deleteBundle:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) {
|
|
120
|
+
BOOL isDeleted = [self removeBundleIfNeeded];
|
|
121
|
+
resolve(@(isDeleted));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
RCT_EXPORT_METHOD(getCurrentVersion:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) {
|
|
125
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
126
|
+
NSString *version = [defaults stringForKey:@"VERSION"];
|
|
127
|
+
if (version) {
|
|
128
|
+
resolve(version);
|
|
129
|
+
} else {
|
|
130
|
+
resolve(@("0"));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
RCT_EXPORT_METHOD(setCurrentVersion:(NSString *)version withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) {
|
|
135
|
+
if (version) {
|
|
136
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
137
|
+
[defaults setObject:version forKey:@"VERSION"];
|
|
138
|
+
[defaults synchronize];
|
|
139
|
+
resolve(@(YES));
|
|
140
|
+
} else {
|
|
141
|
+
resolve(@(NO));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- (void)loadBundle
|
|
146
|
+
{
|
|
147
|
+
RCTTriggerReloadCommandListeners(@"rn-hotupdate: Restart");
|
|
148
|
+
}
|
|
149
|
+
RCT_EXPORT_METHOD(restart) {
|
|
150
|
+
if ([NSThread isMainThread]) {
|
|
151
|
+
[self loadBundle];
|
|
152
|
+
} else {
|
|
153
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
154
|
+
[self loadBundle];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-ota-hot-update",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hot update for react native",
|
|
5
|
+
"main": "src/index",
|
|
6
|
+
"repository": "https://github.com/vantuan88291/react-native-ota-hot-update",
|
|
7
|
+
"author": "vantuan88291 <vantuan88291@gmail.com> (https://github.com/vantuan88291)",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/vantuan88291/react-native-ota-hot-update/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/vantuan88291/react-native-ota-hot-update",
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"react-native-blob-util": ">=0.19.11",
|
|
15
|
+
"react-native": ">=0.63.4"
|
|
16
|
+
},
|
|
17
|
+
"create-react-native-library": {
|
|
18
|
+
"type": "module-legacy",
|
|
19
|
+
"languages": "kotlin-objc",
|
|
20
|
+
"version": "0.41.0"
|
|
21
|
+
},
|
|
22
|
+
"rnpm": {
|
|
23
|
+
"android": {
|
|
24
|
+
"packageInstance": "new OtaHotUpdate(getApplicationContext())"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
|
+
|
|
6
|
+
Pod::Spec.new do |s|
|
|
7
|
+
s.name = "rn-hotupdate"
|
|
8
|
+
s.version = package["version"]
|
|
9
|
+
s.summary = package["description"]
|
|
10
|
+
s.homepage = package["homepage"]
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.authors = package["author"]
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
15
|
+
s.source = { :git => ".git", :tag => "#{s.version}" }
|
|
16
|
+
|
|
17
|
+
s.source_files = "ios/**/*.{h,m,mm}"
|
|
18
|
+
s.dependency 'SSZipArchive', '~> 2.4.3'
|
|
19
|
+
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
20
|
+
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
21
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
22
|
+
install_modules_dependencies(s)
|
|
23
|
+
else
|
|
24
|
+
s.dependency "React-Core"
|
|
25
|
+
|
|
26
|
+
# Don't install the dependencies when we run `pod install` in the old architecture.
|
|
27
|
+
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
|
28
|
+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
29
|
+
s.pod_target_xcconfig = {
|
|
30
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
31
|
+
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
32
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
|
33
|
+
}
|
|
34
|
+
s.dependency "React-Codegen"
|
|
35
|
+
s.dependency "RCT-Folly"
|
|
36
|
+
s.dependency "RCTRequired"
|
|
37
|
+
s.dependency "RCTTypeSafety"
|
|
38
|
+
s.dependency "ReactCommon/turbomodule/core"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
package/src/Utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Platform,
|
|
3
|
+
} from 'react-native';
|
|
4
|
+
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
5
|
+
export const downloadBundleFile = async (uri: string, headers?: object) => {
|
|
6
|
+
const res = await ReactNativeBlobUtil
|
|
7
|
+
.config({
|
|
8
|
+
fileCache: Platform.OS === 'android',
|
|
9
|
+
})
|
|
10
|
+
.fetch('GET', uri, {
|
|
11
|
+
...headers,
|
|
12
|
+
});
|
|
13
|
+
return res.path();
|
|
14
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
import {downloadBundleFile} from './Utils.ts';
|
|
3
|
+
const LINKING_ERROR =
|
|
4
|
+
'The package \'rn-hotupdate\' doesn\'t seem to be linked. Make sure: \n\n' +
|
|
5
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
6
|
+
'- You rebuilt the app after installing the package\n' +
|
|
7
|
+
'- You are not using Expo Go\n';
|
|
8
|
+
|
|
9
|
+
export interface UpdateOption {
|
|
10
|
+
headers?: object
|
|
11
|
+
updateSuccess?(): void
|
|
12
|
+
updateFail?(message?: string): void
|
|
13
|
+
restartAfterInstall?: boolean
|
|
14
|
+
}
|
|
15
|
+
const RNhotupdate = NativeModules.RNhotupdate
|
|
16
|
+
? NativeModules.RNhotupdate
|
|
17
|
+
: new Proxy(
|
|
18
|
+
{},
|
|
19
|
+
{
|
|
20
|
+
get() {
|
|
21
|
+
throw new Error(LINKING_ERROR);
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function setupBundlePath(path: string): Promise<boolean> {
|
|
27
|
+
return RNhotupdate.setupBundlePath(path);
|
|
28
|
+
}
|
|
29
|
+
function deleteBundlePath(): Promise<boolean> {
|
|
30
|
+
return RNhotupdate.deleteBundle();
|
|
31
|
+
}
|
|
32
|
+
function getCurrentVersion(): Promise<string> {
|
|
33
|
+
return RNhotupdate.getCurrentVersion();
|
|
34
|
+
}
|
|
35
|
+
async function getVersionAsNumber() {
|
|
36
|
+
const rawVersion = await getCurrentVersion();
|
|
37
|
+
return +rawVersion;
|
|
38
|
+
}
|
|
39
|
+
function setCurrentVersion(version: number): Promise<boolean> {
|
|
40
|
+
return RNhotupdate.setCurrentVersion(version + '');
|
|
41
|
+
}
|
|
42
|
+
async function resetApp() {
|
|
43
|
+
RNhotupdate.restart();
|
|
44
|
+
}
|
|
45
|
+
function removeBundle(restartAfterRemoved?: boolean) {
|
|
46
|
+
deleteBundlePath().then(data => {
|
|
47
|
+
if (data && restartAfterRemoved) {
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
resetApp();
|
|
50
|
+
}, 300);
|
|
51
|
+
if (data) {
|
|
52
|
+
setCurrentVersion(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const installFail = (option?: UpdateOption, e?: any) => {
|
|
58
|
+
option?.updateFail?.(JSON.stringify(e));
|
|
59
|
+
console.error('Download bundle fail', JSON.stringify(e));
|
|
60
|
+
};
|
|
61
|
+
async function downloadBundleUri(uri: string, version: number, option?: UpdateOption) {
|
|
62
|
+
if (!uri) {
|
|
63
|
+
installFail(option, 'Please give a valid URL!');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!version) {
|
|
67
|
+
installFail(option, 'Please give a valid version!');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const currentVersion = await getVersionAsNumber();
|
|
71
|
+
if (version <= currentVersion) {
|
|
72
|
+
installFail(option, 'Please give a bigger version than the current version, the current version now has setted by: ' + currentVersion);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const path = await downloadBundleFile(uri, option?.headers);
|
|
77
|
+
if (path) {
|
|
78
|
+
setupBundlePath(path).then(success => {
|
|
79
|
+
if (success) {
|
|
80
|
+
setCurrentVersion(version);
|
|
81
|
+
option?.updateSuccess?.();
|
|
82
|
+
if (option?.restartAfterInstall) {
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
resetApp();
|
|
85
|
+
}, 300);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
installFail(option);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
installFail(option);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
installFail(option, e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default {
|
|
100
|
+
setupBundlePath,
|
|
101
|
+
removeUpdate: removeBundle,
|
|
102
|
+
downloadBundleUri,
|
|
103
|
+
resetApp,
|
|
104
|
+
getCurrentVersion: getVersionAsNumber,
|
|
105
|
+
setCurrentVersion,
|
|
106
|
+
};
|