@vanikya/ota-react-native 0.2.3 → 0.2.4
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.
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package com.otaupdate
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Helper object for OTA Update bundle loading.
|
|
10
|
+
* This is called from MainApplication.getJSBundleFile() to get the OTA bundle path.
|
|
11
|
+
*/
|
|
12
|
+
object OTAUpdateHelper {
|
|
13
|
+
private const val TAG = "OTAUpdate"
|
|
14
|
+
private const val PREFS_NAME = "OTAUpdate"
|
|
15
|
+
private const val KEY_BUNDLE_PATH = "BundlePath"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the JS bundle file path for React Native to load.
|
|
19
|
+
* Returns the OTA bundle path if available and valid, otherwise null (loads default bundle).
|
|
20
|
+
*
|
|
21
|
+
* @param context Application context
|
|
22
|
+
* @return Bundle file path or null
|
|
23
|
+
*/
|
|
24
|
+
@JvmStatic
|
|
25
|
+
fun getJSBundleFile(context: Context): String? {
|
|
26
|
+
return try {
|
|
27
|
+
val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
28
|
+
val bundlePath = prefs.getString(KEY_BUNDLE_PATH, null)
|
|
29
|
+
|
|
30
|
+
Log.d(TAG, "getJSBundleFile called, stored path: $bundlePath")
|
|
31
|
+
|
|
32
|
+
if (bundlePath.isNullOrEmpty()) {
|
|
33
|
+
Log.d(TAG, "No OTA bundle path stored, loading default bundle")
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val file = File(bundlePath)
|
|
38
|
+
if (!file.exists()) {
|
|
39
|
+
Log.w(TAG, "OTA bundle file does not exist: $bundlePath")
|
|
40
|
+
// Clear invalid path
|
|
41
|
+
prefs.edit().remove(KEY_BUNDLE_PATH).commit()
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!file.canRead()) {
|
|
46
|
+
Log.w(TAG, "OTA bundle file is not readable: $bundlePath")
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
val fileSize = file.length()
|
|
51
|
+
if (fileSize < 100) {
|
|
52
|
+
Log.w(TAG, "OTA bundle file is too small (${fileSize} bytes), likely corrupted")
|
|
53
|
+
prefs.edit().remove(KEY_BUNDLE_PATH).commit()
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Log.d(TAG, "Loading OTA bundle: $bundlePath ($fileSize bytes)")
|
|
58
|
+
bundlePath
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
Log.e(TAG, "Error getting JS bundle file: ${e.message}", e)
|
|
61
|
+
null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if an OTA bundle is pending to be loaded.
|
|
67
|
+
*
|
|
68
|
+
* @param context Application context
|
|
69
|
+
* @return true if OTA bundle is available
|
|
70
|
+
*/
|
|
71
|
+
@JvmStatic
|
|
72
|
+
fun hasPendingBundle(context: Context): Boolean {
|
|
73
|
+
val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
74
|
+
val bundlePath = prefs.getString(KEY_BUNDLE_PATH, null)
|
|
75
|
+
if (bundlePath.isNullOrEmpty()) return false
|
|
76
|
+
return File(bundlePath).exists()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear the pending OTA bundle.
|
|
81
|
+
*
|
|
82
|
+
* @param context Application context
|
|
83
|
+
*/
|
|
84
|
+
@JvmStatic
|
|
85
|
+
fun clearPendingBundle(context: Context) {
|
|
86
|
+
val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
87
|
+
prefs.edit().remove(KEY_BUNDLE_PATH).commit()
|
|
88
|
+
Log.d(TAG, "Pending bundle cleared")
|
|
89
|
+
}
|
|
90
|
+
}
|
package/app.plugin.js
CHANGED
|
@@ -4,7 +4,7 @@ const { withMainApplication, withAppDelegate } = require('@expo/config-plugins')
|
|
|
4
4
|
* OTA Update Expo Config Plugin
|
|
5
5
|
*
|
|
6
6
|
* This plugin modifies the native code to enable OTA bundle loading:
|
|
7
|
-
* - Android: Overrides getJSBundleFile() in MainApplication.kt
|
|
7
|
+
* - Android: Overrides getJSBundleFile() in MainApplication.kt using OTAUpdateHelper
|
|
8
8
|
* - iOS: Modifies bundleURL() in AppDelegate.swift
|
|
9
9
|
*
|
|
10
10
|
* The modifications check SharedPreferences (Android) / UserDefaults (iOS)
|
|
@@ -16,153 +16,123 @@ function withOTAUpdateAndroid(config) {
|
|
|
16
16
|
let contents = config.modResults.contents;
|
|
17
17
|
|
|
18
18
|
// Check if already modified
|
|
19
|
-
if (contents.includes('
|
|
20
|
-
console.log('[OTAUpdate] Android:
|
|
19
|
+
if (contents.includes('OTAUpdateHelper')) {
|
|
20
|
+
console.log('[OTAUpdate] Android: OTAUpdateHelper already present, skipping');
|
|
21
21
|
return config;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Add
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
contents = contents.slice(0, insertPos) +
|
|
24
|
+
// Add import for OTAUpdateHelper
|
|
25
|
+
const packageMatch = contents.match(/^package\s+[\w.]+\s*\n/m);
|
|
26
|
+
if (packageMatch) {
|
|
27
|
+
const insertPos = packageMatch.index + packageMatch[0].length;
|
|
28
|
+
// Check if import section exists
|
|
29
|
+
if (!contents.includes('import com.otaupdate.OTAUpdateHelper')) {
|
|
30
|
+
const importStatement = `\nimport com.otaupdate.OTAUpdateHelper\n`;
|
|
31
|
+
contents = contents.slice(0, insertPos) + importStatement + contents.slice(insertPos);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const reactNativeHostPattern = /(override\s+val\s+reactNativeHost\s*:\s*ReactNativeHost\s*=\s*object\s*:\s*DefaultReactNativeHost\s*\(\s*this\s*\)\s*\{)/;
|
|
38
|
-
|
|
39
|
-
if (reactNativeHostPattern.test(contents)) {
|
|
40
|
-
const getJSBundleFileOverride = `
|
|
35
|
+
// The getJSBundleFile override that uses our helper
|
|
36
|
+
const getJSBundleFileOverride = `
|
|
41
37
|
override fun getJSBundleFile(): String? {
|
|
42
|
-
|
|
43
|
-
val bundlePath = prefs.getString("BundlePath", null)
|
|
44
|
-
android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
|
|
45
|
-
if (bundlePath != null) {
|
|
46
|
-
val file = File(bundlePath)
|
|
47
|
-
if (file.exists() && file.canRead()) {
|
|
48
|
-
android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
|
|
49
|
-
return bundlePath
|
|
50
|
-
} else {
|
|
51
|
-
android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
android.util.Log.d("OTAUpdate", "Loading default bundle")
|
|
55
|
-
return null
|
|
38
|
+
return OTAUpdateHelper.getJSBundleFile(applicationContext)
|
|
56
39
|
}
|
|
57
40
|
`;
|
|
41
|
+
|
|
42
|
+
// Strategy 1: Look for "object : DefaultReactNativeHost" pattern
|
|
43
|
+
// This handles most Expo SDK 50+ apps
|
|
44
|
+
const defaultHostPattern = /(object\s*:\s*DefaultReactNativeHost\s*\([^)]*\)\s*\{)/;
|
|
45
|
+
|
|
46
|
+
if (defaultHostPattern.test(contents)) {
|
|
47
|
+
contents = contents.replace(
|
|
48
|
+
defaultHostPattern,
|
|
49
|
+
`$1${getJSBundleFileOverride}`
|
|
50
|
+
);
|
|
51
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (DefaultReactNativeHost pattern)');
|
|
52
|
+
config.modResults.contents = contents;
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Strategy 2: Look for "override val reactNativeHost" with object block
|
|
57
|
+
const reactNativeHostPattern = /(override\s+val\s+reactNativeHost[^=]*=\s*object[^{]*\{)/;
|
|
58
|
+
|
|
59
|
+
if (reactNativeHostPattern.test(contents)) {
|
|
58
60
|
contents = contents.replace(
|
|
59
61
|
reactNativeHostPattern,
|
|
60
62
|
`$1${getJSBundleFileOverride}`
|
|
61
63
|
);
|
|
62
|
-
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern
|
|
64
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (reactNativeHost pattern)');
|
|
63
65
|
config.modResults.contents = contents;
|
|
64
66
|
return config;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
// Strategy
|
|
68
|
-
const
|
|
69
|
+
// Strategy 3: Look for any ReactNativeHost object block
|
|
70
|
+
const anyHostPattern = /(ReactNativeHost\s*\([^)]*\)\s*\{)/;
|
|
69
71
|
|
|
70
|
-
if (
|
|
71
|
-
const getJSBundleFileOverride = `
|
|
72
|
-
override fun getJSBundleFile(): String? {
|
|
73
|
-
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
74
|
-
val bundlePath = prefs.getString("BundlePath", null)
|
|
75
|
-
android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
|
|
76
|
-
if (bundlePath != null) {
|
|
77
|
-
val file = File(bundlePath)
|
|
78
|
-
if (file.exists() && file.canRead()) {
|
|
79
|
-
android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
|
|
80
|
-
return bundlePath
|
|
81
|
-
} else {
|
|
82
|
-
android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
android.util.Log.d("OTAUpdate", "Loading default bundle")
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
|
-
`;
|
|
72
|
+
if (anyHostPattern.test(contents)) {
|
|
89
73
|
contents = contents.replace(
|
|
90
|
-
|
|
74
|
+
anyHostPattern,
|
|
91
75
|
`$1${getJSBundleFileOverride}`
|
|
92
76
|
);
|
|
93
|
-
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern
|
|
77
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (generic ReactNativeHost pattern)');
|
|
94
78
|
config.modResults.contents = contents;
|
|
95
79
|
return config;
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
// Strategy
|
|
99
|
-
|
|
82
|
+
// Strategy 4: Find the class and look for the host definition more flexibly
|
|
83
|
+
// Look for "override fun getUseDeveloperSupport" and insert before it
|
|
84
|
+
const devSupportPattern = /([\t ]*)(override\s+fun\s+getUseDeveloperSupport)/;
|
|
100
85
|
|
|
101
86
|
if (devSupportPattern.test(contents)) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
android.util.Log.d("OTAUpdate", "getJSBundleFile called, stored path: $bundlePath")
|
|
106
|
-
if (bundlePath != null) {
|
|
107
|
-
val file = File(bundlePath)
|
|
108
|
-
if (file.exists() && file.canRead()) {
|
|
109
|
-
android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
|
|
110
|
-
return bundlePath
|
|
111
|
-
} else {
|
|
112
|
-
android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
android.util.Log.d("OTAUpdate", "Loading default bundle")
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
`;
|
|
87
|
+
const indentMatch = contents.match(devSupportPattern);
|
|
88
|
+
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
89
|
+
const overrideCode = `${indent}override fun getJSBundleFile(): String? {\n${indent} return OTAUpdateHelper.getJSBundleFile(applicationContext)\n${indent}}\n\n${indent}`;
|
|
120
90
|
contents = contents.replace(
|
|
121
91
|
devSupportPattern,
|
|
122
|
-
`${
|
|
92
|
+
`${overrideCode}$2`
|
|
123
93
|
);
|
|
124
|
-
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (
|
|
94
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (before getUseDeveloperSupport)');
|
|
125
95
|
config.modResults.contents = contents;
|
|
126
96
|
return config;
|
|
127
97
|
}
|
|
128
98
|
|
|
129
|
-
// Strategy
|
|
130
|
-
|
|
99
|
+
// Strategy 5: Last resort - find any class with Application and inject
|
|
100
|
+
// Look for the ReactApplication interface implementation
|
|
101
|
+
const reactAppPattern = /(class\s+\w+\s*:\s*Application\s*\(\s*\)\s*,\s*ReactApplication\s*\{)/;
|
|
131
102
|
|
|
132
|
-
if (
|
|
133
|
-
// Find
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
const insertPos =
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (bundlePath != null) {
|
|
143
|
-
val file = File(bundlePath)
|
|
144
|
-
if (file.exists() && file.canRead()) {
|
|
145
|
-
android.util.Log.d("OTAUpdate", "Loading OTA bundle: $bundlePath (${file.length()} bytes)")
|
|
146
|
-
return bundlePath
|
|
147
|
-
} else {
|
|
148
|
-
android.util.Log.w("OTAUpdate", "OTA bundle not found or not readable: $bundlePath, exists=${file.exists()}")
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
android.util.Log.d("OTAUpdate", "Loading default bundle")
|
|
152
|
-
return null
|
|
153
|
-
}
|
|
103
|
+
if (reactAppPattern.test(contents)) {
|
|
104
|
+
// Find where to inject - after the class opening
|
|
105
|
+
const classMatch = contents.match(reactAppPattern);
|
|
106
|
+
if (classMatch) {
|
|
107
|
+
const insertPos = classMatch.index + classMatch[0].length;
|
|
108
|
+
const helperComment = `
|
|
109
|
+
// OTA Update: Override to load OTA bundle if available
|
|
110
|
+
private fun getOTABundleFile(): String? {
|
|
111
|
+
return OTAUpdateHelper.getJSBundleFile(applicationContext)
|
|
112
|
+
}
|
|
154
113
|
`;
|
|
155
|
-
contents = contents.slice(0, insertPos) +
|
|
156
|
-
console.log('[OTAUpdate] Android:
|
|
157
|
-
|
|
158
|
-
return config;
|
|
114
|
+
contents = contents.slice(0, insertPos) + helperComment + contents.slice(insertPos);
|
|
115
|
+
console.log('[OTAUpdate] Android: Added OTA helper method to Application class');
|
|
116
|
+
console.log('[OTAUpdate] Android: WARNING - You may need to manually wire getJSBundleFile() override');
|
|
159
117
|
}
|
|
160
118
|
}
|
|
161
119
|
|
|
162
|
-
|
|
163
|
-
console.warn('[OTAUpdate] Android:
|
|
120
|
+
// Log the current MainApplication structure for debugging
|
|
121
|
+
console.warn('[OTAUpdate] Android: Could not find standard injection point');
|
|
122
|
+
console.warn('[OTAUpdate] Android: Please ensure your MainApplication.kt has a ReactNativeHost definition');
|
|
123
|
+
console.warn('[OTAUpdate] Android: You may need to manually add the getJSBundleFile override');
|
|
164
124
|
console.warn('[OTAUpdate] Android: See https://vanikya.github.io/ota-update/ for manual setup instructions');
|
|
165
125
|
|
|
126
|
+
// Log first 100 lines for debugging
|
|
127
|
+
const lines = contents.split('\n').slice(0, 100);
|
|
128
|
+
console.log('[OTAUpdate] Android: First 100 lines of MainApplication.kt:');
|
|
129
|
+
lines.forEach((line, i) => {
|
|
130
|
+
if (line.includes('ReactNativeHost') || line.includes('DefaultReactNativeHost') ||
|
|
131
|
+
line.includes('getJSBundleFile') || line.includes('override')) {
|
|
132
|
+
console.log(` ${i + 1}: ${line}`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
166
136
|
config.modResults.contents = contents;
|
|
167
137
|
return config;
|
|
168
138
|
});
|
|
@@ -196,6 +166,8 @@ function withOTAUpdateIOS(config) {
|
|
|
196
166
|
return URL(fileURLWithPath: path)
|
|
197
167
|
} else {
|
|
198
168
|
NSLog("[OTAUpdate] OTA bundle not found at path: %@", path)
|
|
169
|
+
// Clear invalid path
|
|
170
|
+
UserDefaults.standard.removeObject(forKey: "OTAUpdateBundlePath")
|
|
199
171
|
}
|
|
200
172
|
}
|
|
201
173
|
NSLog("[OTAUpdate] Loading default bundle")
|
|
@@ -204,14 +176,12 @@ function withOTAUpdateIOS(config) {
|
|
|
204
176
|
`;
|
|
205
177
|
|
|
206
178
|
// Strategy 1: Look for bundleURL() with flexible pattern
|
|
207
|
-
// Match: func bundleURL() -> URL? { ... } with any content inside
|
|
208
179
|
const bundleURLPattern1 = /(func\s+bundleURL\s*\(\s*\)\s*->\s*URL\?\s*\{)([\s\S]*?)(\n\s*\})/;
|
|
209
180
|
|
|
210
181
|
if (bundleURLPattern1.test(contents)) {
|
|
211
182
|
contents = contents.replace(
|
|
212
183
|
bundleURLPattern1,
|
|
213
184
|
(match, funcStart, funcBody, funcEnd) => {
|
|
214
|
-
// Check if it already has OTA check
|
|
215
185
|
if (funcBody.includes('getOTABundleURL')) {
|
|
216
186
|
return match;
|
|
217
187
|
}
|
|
@@ -291,8 +261,10 @@ ${helperFunction}
|
|
|
291
261
|
// Check for OTA bundle
|
|
292
262
|
NSString *bundlePath = [[NSUserDefaults standardUserDefaults] stringForKey:@"OTAUpdateBundlePath"];
|
|
293
263
|
if (bundlePath && [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
|
|
264
|
+
NSLog(@"[OTAUpdate] Loading OTA bundle: %@", bundlePath);
|
|
294
265
|
return [NSURL fileURLWithPath:bundlePath];
|
|
295
266
|
}
|
|
267
|
+
NSLog(@"[OTAUpdate] Loading default bundle");
|
|
296
268
|
`;
|
|
297
269
|
contents = contents.replace(
|
|
298
270
|
sourceURLPattern,
|
package/lib/commonjs/index.js
CHANGED
package/lib/module/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { OTAApiClient, getDeviceInfo } from './utils/api';
|
|
|
10
10
|
export { UpdateStorage, getStorageAdapter } from './utils/storage';
|
|
11
11
|
export { calculateHash, verifyBundleHash, verifySignature, verifyBundle } from './utils/verification';
|
|
12
12
|
// Version info
|
|
13
|
-
export const VERSION = '0.2.
|
|
13
|
+
export const VERSION = '0.2.4';
|
|
14
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -9,5 +9,5 @@ export { UpdateStorage, getStorageAdapter } from './utils/storage';
|
|
|
9
9
|
export type { StoredUpdate, StorageAdapter } from './utils/storage';
|
|
10
10
|
export { calculateHash, verifyBundleHash, verifySignature, verifyBundle, } from './utils/verification';
|
|
11
11
|
export type { VerificationResult } from './utils/verification';
|
|
12
|
-
export declare const VERSION = "0.2.
|
|
12
|
+
export declare const VERSION = "0.2.4";
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
package/src/index.ts
CHANGED