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 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
+ [![npm downloads](https://img.shields.io/npm/dw/react-native-ota-hot-update)](https://img.shields.io/npm/dw/react-native-ota-hot-update)
11
+ [![npm package](https://img.shields.io/npm/v/react-native-ota-hot-update?color=red)](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
+ ![](https://github.com/vantuan88291/react-native-ota-hot-update/raw/main/scr1.png)
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,5 @@
1
+ RNhotupdate_kotlinVersion=1.7.0
2
+ RNhotupdate_minSdkVersion=21
3
+ RNhotupdate_targetSdkVersion=31
4
+ RNhotupdate_compileSdkVersion=31
5
+ RNhotupdate_ndkversion=21.4.7075529
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.rnhotupdate">
3
+ </manifest>
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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,9 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageInstance: 'new OtaHotUpdate(getApplicationContext())',
6
+ },
7
+ },
8
+ },
9
+ };
@@ -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
+ };