@vanikya/ota-react-native 0.1.8 → 0.2.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/README.md +135 -1
- package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +94 -0
- package/app.plugin.js +201 -21
- package/ios/OTAUpdate.m +9 -0
- package/ios/OTAUpdate.swift +106 -0
- package/lib/commonjs/hooks/useOTAUpdate.js +22 -10
- package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -1
- package/lib/commonjs/utils/storage.js +149 -0
- package/lib/commonjs/utils/storage.js.map +1 -1
- package/lib/module/hooks/useOTAUpdate.js +22 -10
- package/lib/module/hooks/useOTAUpdate.js.map +1 -1
- package/lib/module/utils/storage.js +149 -0
- package/lib/module/utils/storage.js.map +1 -1
- package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -1
- package/lib/typescript/utils/storage.d.ts +24 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useOTAUpdate.ts +24 -16
- package/src/utils/storage.ts +170 -0
package/README.md
CHANGED
|
@@ -46,12 +46,101 @@ eas build --platform ios
|
|
|
46
46
|
|
|
47
47
|
### For bare React Native apps
|
|
48
48
|
|
|
49
|
-
Install pods (iOS):
|
|
49
|
+
1. Install pods (iOS):
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
cd ios && pod install
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
2. **IMPORTANT**: You must manually configure native code to load OTA bundles. Without this, downloaded bundles will never be applied.
|
|
56
|
+
|
|
57
|
+
#### Android Setup (MainApplication.kt)
|
|
58
|
+
|
|
59
|
+
Add the `getJSBundleFile()` override inside your `ReactNativeHost`:
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
import android.content.SharedPreferences
|
|
63
|
+
import java.io.File
|
|
64
|
+
|
|
65
|
+
// Inside your MainApplication class, find the ReactNativeHost and add:
|
|
66
|
+
override fun getJSBundleFile(): String? {
|
|
67
|
+
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
68
|
+
val bundlePath = prefs.getString("BundlePath", null)
|
|
69
|
+
if (bundlePath != null && File(bundlePath).exists()) {
|
|
70
|
+
return bundlePath
|
|
71
|
+
}
|
|
72
|
+
return null // Falls back to default bundle
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Example full `MainApplication.kt`:
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
package com.yourapp
|
|
80
|
+
|
|
81
|
+
import android.app.Application
|
|
82
|
+
import android.content.SharedPreferences
|
|
83
|
+
import java.io.File
|
|
84
|
+
import com.facebook.react.PackageList
|
|
85
|
+
import com.facebook.react.ReactApplication
|
|
86
|
+
import com.facebook.react.ReactHost
|
|
87
|
+
import com.facebook.react.ReactNativeHost
|
|
88
|
+
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
89
|
+
|
|
90
|
+
class MainApplication : Application(), ReactApplication {
|
|
91
|
+
|
|
92
|
+
override val reactNativeHost: ReactNativeHost =
|
|
93
|
+
object : DefaultReactNativeHost(this) {
|
|
94
|
+
// ADD THIS METHOD for OTA updates
|
|
95
|
+
override fun getJSBundleFile(): String? {
|
|
96
|
+
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
97
|
+
val bundlePath = prefs.getString("BundlePath", null)
|
|
98
|
+
if (bundlePath != null && File(bundlePath).exists()) {
|
|
99
|
+
return bundlePath
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun getPackages() = PackageList(this).packages
|
|
105
|
+
override fun getJSMainModuleName() = "index"
|
|
106
|
+
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
|
|
107
|
+
override val isNewArchEnabled = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
|
108
|
+
override val isHermesEnabled = BuildConfig.IS_HERMES_ENABLED
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ... rest of your MainApplication
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### iOS Setup (AppDelegate.swift)
|
|
116
|
+
|
|
117
|
+
Modify `bundleURL()` to check for OTA bundles first:
|
|
118
|
+
|
|
119
|
+
```swift
|
|
120
|
+
// Add this helper function inside AppDelegate class
|
|
121
|
+
private func getOTABundleURL() -> URL? {
|
|
122
|
+
if let bundlePath = UserDefaults.standard.string(forKey: "OTAUpdateBundlePath") {
|
|
123
|
+
if FileManager.default.fileExists(atPath: bundlePath) {
|
|
124
|
+
return URL(fileURLWithPath: bundlePath)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Modify or add this bundleURL function
|
|
131
|
+
func bundleURL() -> URL? {
|
|
132
|
+
// Check for downloaded OTA bundle first
|
|
133
|
+
if let otaBundle = getOTABundleURL() {
|
|
134
|
+
return otaBundle
|
|
135
|
+
}
|
|
136
|
+
#if DEBUG
|
|
137
|
+
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
|
138
|
+
#else
|
|
139
|
+
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
140
|
+
#endif
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
55
144
|
## Quick Start
|
|
56
145
|
|
|
57
146
|
### 1. Wrap your app with OTAProvider
|
|
@@ -339,6 +428,51 @@ async function recoverFromCorruptedUpdate() {
|
|
|
339
428
|
|
|
340
429
|
The SDK now validates downloaded bundles to prevent HTML error pages or corrupted files from being saved as bundles.
|
|
341
430
|
|
|
431
|
+
## Troubleshooting
|
|
432
|
+
|
|
433
|
+
### Update downloads but doesn't apply after restart
|
|
434
|
+
|
|
435
|
+
This is usually because the native code to load OTA bundles is missing. Check:
|
|
436
|
+
|
|
437
|
+
1. **For Expo apps**: Make sure you rebuilt the app with EAS Build after adding the plugin. OTA updates do NOT work in Expo Go.
|
|
438
|
+
|
|
439
|
+
2. **For bare React Native**: Make sure you added the native code changes described in the "For bare React Native apps" section above.
|
|
440
|
+
|
|
441
|
+
3. **Verify native code was injected**: During the EAS build, look for these log messages:
|
|
442
|
+
- `[OTAUpdate] Android: Successfully injected getJSBundleFile`
|
|
443
|
+
- `[OTAUpdate] iOS: Successfully modified bundleURL`
|
|
444
|
+
|
|
445
|
+
4. **Check if bundle path is saved**: The path is stored in:
|
|
446
|
+
- Android: SharedPreferences key `"BundlePath"` in `"OTAUpdate"` preferences
|
|
447
|
+
- iOS: UserDefaults key `"OTAUpdateBundlePath"`
|
|
448
|
+
|
|
449
|
+
### Bundle downloads but hash verification fails
|
|
450
|
+
|
|
451
|
+
1. Make sure your server is returning the actual JavaScript bundle, not an HTML error page
|
|
452
|
+
2. Check that the bundle wasn't corrupted during upload
|
|
453
|
+
3. Try re-publishing the release with `ota release`
|
|
454
|
+
|
|
455
|
+
### App crashes after OTA update
|
|
456
|
+
|
|
457
|
+
Call `clearCorruptedBundle()` on app startup to recover:
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
import { UpdateStorage } from '@vanikya/ota-react-native';
|
|
461
|
+
|
|
462
|
+
// In your App.tsx or entry point
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
const storage = new UpdateStorage();
|
|
465
|
+
storage.clearCorruptedBundle();
|
|
466
|
+
}, []);
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Debugging
|
|
470
|
+
|
|
471
|
+
Enable debug logs by checking `__DEV__` console output. The SDK logs:
|
|
472
|
+
- `[OTAUpdate] Bundle registered with native module`
|
|
473
|
+
- `[OTAUpdate] Bundle verified successfully`
|
|
474
|
+
- `[OTAUpdate] Update ready`
|
|
475
|
+
|
|
342
476
|
## License
|
|
343
477
|
|
|
344
478
|
MIT
|
|
@@ -5,6 +5,10 @@ import android.content.SharedPreferences
|
|
|
5
5
|
import android.util.Base64
|
|
6
6
|
import com.facebook.react.bridge.*
|
|
7
7
|
import java.io.File
|
|
8
|
+
import java.io.FileOutputStream
|
|
9
|
+
import java.io.InputStream
|
|
10
|
+
import java.net.HttpURLConnection
|
|
11
|
+
import java.net.URL
|
|
8
12
|
import java.security.MessageDigest
|
|
9
13
|
|
|
10
14
|
class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
@@ -92,6 +96,65 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
// Download file directly to disk - bypasses JS memory entirely
|
|
100
|
+
// This is critical for large bundles (5MB+)
|
|
101
|
+
@ReactMethod
|
|
102
|
+
fun downloadFile(urlString: String, destPath: String, promise: Promise) {
|
|
103
|
+
Thread {
|
|
104
|
+
var connection: HttpURLConnection? = null
|
|
105
|
+
var inputStream: InputStream? = null
|
|
106
|
+
var outputStream: FileOutputStream? = null
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
val url = URL(urlString)
|
|
110
|
+
connection = url.openConnection() as HttpURLConnection
|
|
111
|
+
connection.connectTimeout = 30000
|
|
112
|
+
connection.readTimeout = 60000
|
|
113
|
+
connection.requestMethod = "GET"
|
|
114
|
+
connection.connect()
|
|
115
|
+
|
|
116
|
+
val responseCode = connection.responseCode
|
|
117
|
+
if (responseCode != HttpURLConnection.HTTP_OK) {
|
|
118
|
+
promise.reject("DOWNLOAD_ERROR", "Download failed with status $responseCode")
|
|
119
|
+
return@Thread
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Ensure parent directory exists
|
|
123
|
+
val destFile = File(destPath)
|
|
124
|
+
destFile.parentFile?.mkdirs()
|
|
125
|
+
|
|
126
|
+
inputStream = connection.inputStream
|
|
127
|
+
outputStream = FileOutputStream(destFile)
|
|
128
|
+
|
|
129
|
+
val buffer = ByteArray(8192) // 8KB buffer for efficient streaming
|
|
130
|
+
var bytesRead: Int
|
|
131
|
+
var totalBytesRead: Long = 0
|
|
132
|
+
|
|
133
|
+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
134
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
135
|
+
totalBytesRead += bytesRead
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
outputStream.flush()
|
|
139
|
+
|
|
140
|
+
val result = Arguments.createMap()
|
|
141
|
+
result.putDouble("fileSize", totalBytesRead.toDouble())
|
|
142
|
+
promise.resolve(result)
|
|
143
|
+
|
|
144
|
+
} catch (e: Exception) {
|
|
145
|
+
promise.reject("DOWNLOAD_ERROR", "Failed to download file: ${e.message}", e)
|
|
146
|
+
} finally {
|
|
147
|
+
try {
|
|
148
|
+
inputStream?.close()
|
|
149
|
+
outputStream?.close()
|
|
150
|
+
connection?.disconnect()
|
|
151
|
+
} catch (e: Exception) {
|
|
152
|
+
// Ignore cleanup errors
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}.start()
|
|
156
|
+
}
|
|
157
|
+
|
|
95
158
|
// Cryptography
|
|
96
159
|
|
|
97
160
|
@ReactMethod
|
|
@@ -107,6 +170,37 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
|
|
|
107
170
|
}
|
|
108
171
|
}
|
|
109
172
|
|
|
173
|
+
// Calculate SHA256 from file path - streams file to avoid memory issues
|
|
174
|
+
// Critical for large bundles (5MB+)
|
|
175
|
+
@ReactMethod
|
|
176
|
+
fun calculateSHA256FromFile(filePath: String, promise: Promise) {
|
|
177
|
+
Thread {
|
|
178
|
+
try {
|
|
179
|
+
val file = File(filePath)
|
|
180
|
+
if (!file.exists()) {
|
|
181
|
+
promise.reject("FILE_ERROR", "File not found: $filePath")
|
|
182
|
+
return@Thread
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
186
|
+
val buffer = ByteArray(8192) // 8KB buffer
|
|
187
|
+
var bytesRead: Int
|
|
188
|
+
|
|
189
|
+
file.inputStream().use { inputStream ->
|
|
190
|
+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
191
|
+
digest.update(buffer, 0, bytesRead)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val hash = digest.digest()
|
|
196
|
+
val hexString = hash.joinToString("") { "%02x".format(it) }
|
|
197
|
+
promise.resolve(hexString)
|
|
198
|
+
} catch (e: Exception) {
|
|
199
|
+
promise.reject("HASH_ERROR", "Failed to calculate hash: ${e.message}", e)
|
|
200
|
+
}
|
|
201
|
+
}.start()
|
|
202
|
+
}
|
|
203
|
+
|
|
110
204
|
@ReactMethod
|
|
111
205
|
fun verifySignature(base64Content: String, signatureHex: String, publicKeyHex: String, promise: Promise) {
|
|
112
206
|
// Ed25519 signature verification
|
package/app.plugin.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
const { withMainApplication, withAppDelegate
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const { withMainApplication, withAppDelegate } = require('@expo/config-plugins');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OTA Update Expo Config Plugin
|
|
5
|
+
*
|
|
6
|
+
* This plugin modifies the native code to enable OTA bundle loading:
|
|
7
|
+
* - Android: Overrides getJSBundleFile() in MainApplication.kt
|
|
8
|
+
* - iOS: Modifies bundleURL() in AppDelegate.swift
|
|
9
|
+
*
|
|
10
|
+
* The modifications check SharedPreferences (Android) / UserDefaults (iOS)
|
|
11
|
+
* for a pending OTA bundle path and load it instead of the default bundle.
|
|
12
|
+
*/
|
|
4
13
|
|
|
5
14
|
function withOTAUpdateAndroid(config) {
|
|
6
15
|
return withMainApplication(config, (config) => {
|
|
@@ -8,25 +17,73 @@ function withOTAUpdateAndroid(config) {
|
|
|
8
17
|
|
|
9
18
|
// Check if already modified
|
|
10
19
|
if (contents.includes('getJSBundleFile')) {
|
|
20
|
+
console.log('[OTAUpdate] Android: getJSBundleFile already present, skipping');
|
|
11
21
|
return config;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
// Add imports if not present
|
|
15
25
|
if (!contents.includes('import android.content.SharedPreferences')) {
|
|
26
|
+
// Find the package declaration and add imports after it
|
|
27
|
+
const packageMatch = contents.match(/^package\s+[\w.]+\s*\n/m);
|
|
28
|
+
if (packageMatch) {
|
|
29
|
+
const insertPos = packageMatch.index + packageMatch[0].length;
|
|
30
|
+
const imports = `\nimport android.content.SharedPreferences\nimport java.io.File\n`;
|
|
31
|
+
contents = contents.slice(0, insertPos) + imports + contents.slice(insertPos);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Strategy 1: Look for "override val reactNativeHost" pattern (Expo SDK 50+)
|
|
36
|
+
// This is more reliable than matching getUseDeveloperSupport
|
|
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 = `
|
|
41
|
+
override fun getJSBundleFile(): String? {
|
|
42
|
+
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
43
|
+
val bundlePath = prefs.getString("BundlePath", null)
|
|
44
|
+
if (bundlePath != null && File(bundlePath).exists()) {
|
|
45
|
+
return bundlePath
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
16
50
|
contents = contents.replace(
|
|
17
|
-
|
|
18
|
-
`$1
|
|
51
|
+
reactNativeHostPattern,
|
|
52
|
+
`$1${getJSBundleFileOverride}`
|
|
19
53
|
);
|
|
54
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 1)');
|
|
55
|
+
config.modResults.contents = contents;
|
|
56
|
+
return config;
|
|
20
57
|
}
|
|
21
58
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const kotlinPattern = /override\s+fun\s+getUseDeveloperSupport\(\):\s+Boolean\s*=\s*BuildConfig\.DEBUG/;
|
|
59
|
+
// Strategy 2: Look for DefaultReactNativeHost with different formatting
|
|
60
|
+
const altPattern = /(object\s*:\s*DefaultReactNativeHost\s*\(\s*this\s*\)\s*\{)/;
|
|
25
61
|
|
|
26
|
-
if (
|
|
62
|
+
if (altPattern.test(contents)) {
|
|
63
|
+
const getJSBundleFileOverride = `
|
|
64
|
+
override fun getJSBundleFile(): String? {
|
|
65
|
+
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
66
|
+
val bundlePath = prefs.getString("BundlePath", null)
|
|
67
|
+
if (bundlePath != null && File(bundlePath).exists()) {
|
|
68
|
+
return bundlePath
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
27
73
|
contents = contents.replace(
|
|
28
|
-
|
|
29
|
-
`
|
|
74
|
+
altPattern,
|
|
75
|
+
`$1${getJSBundleFileOverride}`
|
|
76
|
+
);
|
|
77
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 2)');
|
|
78
|
+
config.modResults.contents = contents;
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Strategy 3: Look for getUseDeveloperSupport with flexible whitespace
|
|
83
|
+
const devSupportPattern = /(override\s+fun\s+getUseDeveloperSupport\s*\(\s*\)\s*[:\s]*Boolean\s*[=\{])/;
|
|
84
|
+
|
|
85
|
+
if (devSupportPattern.test(contents)) {
|
|
86
|
+
const getJSBundleFileOverride = `override fun getJSBundleFile(): String? {
|
|
30
87
|
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
31
88
|
val bundlePath = prefs.getString("BundlePath", null)
|
|
32
89
|
if (bundlePath != null && File(bundlePath).exists()) {
|
|
@@ -35,10 +92,45 @@ function withOTAUpdateAndroid(config) {
|
|
|
35
92
|
return null
|
|
36
93
|
}
|
|
37
94
|
|
|
38
|
-
|
|
95
|
+
`;
|
|
96
|
+
contents = contents.replace(
|
|
97
|
+
devSupportPattern,
|
|
98
|
+
`${getJSBundleFileOverride}$1`
|
|
39
99
|
);
|
|
100
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 3)');
|
|
101
|
+
config.modResults.contents = contents;
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Strategy 4: Look for ReactNativeHost in any form
|
|
106
|
+
const genericPattern = /(ReactNativeHost\s*[\(\{])/;
|
|
107
|
+
|
|
108
|
+
if (genericPattern.test(contents)) {
|
|
109
|
+
// Find the opening brace after ReactNativeHost and insert after it
|
|
110
|
+
const match = contents.match(/ReactNativeHost[^{]*\{/);
|
|
111
|
+
if (match) {
|
|
112
|
+
const insertPos = match.index + match[0].length;
|
|
113
|
+
const getJSBundleFileOverride = `
|
|
114
|
+
override fun getJSBundleFile(): String? {
|
|
115
|
+
val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
|
|
116
|
+
val bundlePath = prefs.getString("BundlePath", null)
|
|
117
|
+
if (bundlePath != null && File(bundlePath).exists()) {
|
|
118
|
+
return bundlePath
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
contents = contents.slice(0, insertPos) + getJSBundleFileOverride + contents.slice(insertPos);
|
|
124
|
+
console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (pattern 4)');
|
|
125
|
+
config.modResults.contents = contents;
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
40
128
|
}
|
|
41
129
|
|
|
130
|
+
console.warn('[OTAUpdate] Android: Could not find suitable injection point for getJSBundleFile');
|
|
131
|
+
console.warn('[OTAUpdate] Android: Please manually add getJSBundleFile override to MainApplication.kt');
|
|
132
|
+
console.warn('[OTAUpdate] Android: See https://vanikya.github.io/ota-update/ for manual setup instructions');
|
|
133
|
+
|
|
42
134
|
config.modResults.contents = contents;
|
|
43
135
|
return config;
|
|
44
136
|
});
|
|
@@ -50,17 +142,17 @@ function withOTAUpdateIOS(config) {
|
|
|
50
142
|
|
|
51
143
|
// Check if already modified
|
|
52
144
|
if (contents.includes('OTAUpdateBundlePath')) {
|
|
145
|
+
console.log('[OTAUpdate] iOS: OTAUpdateBundlePath already present, skipping');
|
|
53
146
|
return config;
|
|
54
147
|
}
|
|
55
148
|
|
|
56
149
|
// For Swift AppDelegate
|
|
57
150
|
if (config.modResults.language === 'swift') {
|
|
58
|
-
//
|
|
151
|
+
// Helper function to get OTA bundle URL
|
|
59
152
|
const helperFunction = `
|
|
60
153
|
// OTA Update: Check for downloaded bundle
|
|
61
154
|
private func getOTABundleURL() -> URL? {
|
|
62
|
-
let
|
|
63
|
-
if let bundlePath = defaults.string(forKey: "OTAUpdateBundlePath") {
|
|
155
|
+
if let bundlePath = UserDefaults.standard.string(forKey: "OTAUpdateBundlePath") {
|
|
64
156
|
let fileURL = URL(fileURLWithPath: bundlePath)
|
|
65
157
|
if FileManager.default.fileExists(atPath: bundlePath) {
|
|
66
158
|
return fileURL
|
|
@@ -70,25 +162,109 @@ function withOTAUpdateIOS(config) {
|
|
|
70
162
|
}
|
|
71
163
|
`;
|
|
72
164
|
|
|
73
|
-
//
|
|
74
|
-
|
|
165
|
+
// Strategy 1: Look for bundleURL() with flexible pattern
|
|
166
|
+
// Match: func bundleURL() -> URL? { ... } with any content inside
|
|
167
|
+
const bundleURLPattern1 = /(func\s+bundleURL\s*\(\s*\)\s*->\s*URL\?\s*\{)([\s\S]*?)(\n\s*\})/;
|
|
168
|
+
|
|
169
|
+
if (bundleURLPattern1.test(contents)) {
|
|
170
|
+
contents = contents.replace(
|
|
171
|
+
bundleURLPattern1,
|
|
172
|
+
(match, funcStart, funcBody, funcEnd) => {
|
|
173
|
+
// Check if it already has OTA check
|
|
174
|
+
if (funcBody.includes('getOTABundleURL')) {
|
|
175
|
+
return match;
|
|
176
|
+
}
|
|
177
|
+
return `${funcStart}
|
|
178
|
+
// OTA Update: Check for downloaded bundle first
|
|
179
|
+
if let otaBundle = getOTABundleURL() {
|
|
180
|
+
return otaBundle
|
|
181
|
+
}
|
|
182
|
+
${funcBody}${funcEnd}${helperFunction}`;
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
console.log('[OTAUpdate] iOS: Successfully modified bundleURL (pattern 1)');
|
|
186
|
+
config.modResults.contents = contents;
|
|
187
|
+
return config;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Strategy 2: Look for sourceURL(for bridge:) pattern (older Expo versions)
|
|
191
|
+
const sourceURLPattern = /(func\s+sourceURL\s*\(\s*for\s+bridge\s*:\s*RCTBridge\s*\)\s*->\s*URL\?\s*\{)([\s\S]*?)(\n\s*\})/;
|
|
75
192
|
|
|
76
|
-
if (
|
|
193
|
+
if (sourceURLPattern.test(contents)) {
|
|
77
194
|
contents = contents.replace(
|
|
78
|
-
|
|
79
|
-
|
|
195
|
+
sourceURLPattern,
|
|
196
|
+
(match, funcStart, funcBody, funcEnd) => {
|
|
197
|
+
if (funcBody.includes('getOTABundleURL')) {
|
|
198
|
+
return match;
|
|
199
|
+
}
|
|
200
|
+
return `${funcStart}
|
|
80
201
|
// OTA Update: Check for downloaded bundle first
|
|
81
202
|
if let otaBundle = getOTABundleURL() {
|
|
82
203
|
return otaBundle
|
|
83
204
|
}
|
|
205
|
+
${funcBody}${funcEnd}${helperFunction}`;
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
console.log('[OTAUpdate] iOS: Successfully modified sourceURL (pattern 2)');
|
|
209
|
+
config.modResults.contents = contents;
|
|
210
|
+
return config;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Strategy 3: Add bundleURL method if it doesn't exist but class exists
|
|
214
|
+
const appDelegateClassPattern = /(class\s+AppDelegate\s*[^{]*\{)/;
|
|
215
|
+
|
|
216
|
+
if (appDelegateClassPattern.test(contents) && !contents.includes('func bundleURL')) {
|
|
217
|
+
const newBundleURLMethod = `
|
|
218
|
+
// OTA Update: Bundle URL with OTA support
|
|
219
|
+
func bundleURL() -> URL? {
|
|
220
|
+
// Check for downloaded OTA bundle first
|
|
221
|
+
if let otaBundle = getOTABundleURL() {
|
|
222
|
+
return otaBundle
|
|
223
|
+
}
|
|
84
224
|
#if DEBUG
|
|
85
225
|
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
|
86
226
|
#else
|
|
87
227
|
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
88
228
|
#endif
|
|
89
229
|
}
|
|
90
|
-
${helperFunction}
|
|
230
|
+
${helperFunction}
|
|
231
|
+
`;
|
|
232
|
+
contents = contents.replace(
|
|
233
|
+
appDelegateClassPattern,
|
|
234
|
+
`$1${newBundleURLMethod}`
|
|
91
235
|
);
|
|
236
|
+
console.log('[OTAUpdate] iOS: Successfully added bundleURL method (pattern 3)');
|
|
237
|
+
config.modResults.contents = contents;
|
|
238
|
+
return config;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.warn('[OTAUpdate] iOS: Could not find suitable injection point');
|
|
242
|
+
console.warn('[OTAUpdate] iOS: Please manually modify AppDelegate.swift');
|
|
243
|
+
console.warn('[OTAUpdate] iOS: See https://vanikya.github.io/ota-update/ for manual setup instructions');
|
|
244
|
+
} else if (config.modResults.language === 'objc' || config.modResults.language === 'objcpp') {
|
|
245
|
+
// Objective-C AppDelegate (older Expo versions)
|
|
246
|
+
const sourceURLPattern = /(- \(NSURL \*\)sourceURLForBridge:\(RCTBridge \*\)bridge\s*\{)([\s\S]*?)(\n\})/;
|
|
247
|
+
|
|
248
|
+
if (sourceURLPattern.test(contents)) {
|
|
249
|
+
const helperCode = `
|
|
250
|
+
// Check for OTA bundle
|
|
251
|
+
NSString *bundlePath = [[NSUserDefaults standardUserDefaults] stringForKey:@"OTAUpdateBundlePath"];
|
|
252
|
+
if (bundlePath && [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
|
|
253
|
+
return [NSURL fileURLWithPath:bundlePath];
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
contents = contents.replace(
|
|
257
|
+
sourceURLPattern,
|
|
258
|
+
(match, funcStart, funcBody, funcEnd) => {
|
|
259
|
+
if (funcBody.includes('OTAUpdateBundlePath')) {
|
|
260
|
+
return match;
|
|
261
|
+
}
|
|
262
|
+
return `${funcStart}${helperCode}${funcBody}${funcEnd}`;
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
console.log('[OTAUpdate] iOS: Successfully modified sourceURLForBridge (Obj-C)');
|
|
266
|
+
config.modResults.contents = contents;
|
|
267
|
+
return config;
|
|
92
268
|
}
|
|
93
269
|
}
|
|
94
270
|
|
|
@@ -102,3 +278,7 @@ module.exports = function withOTAUpdate(config) {
|
|
|
102
278
|
config = withOTAUpdateIOS(config);
|
|
103
279
|
return config;
|
|
104
280
|
};
|
|
281
|
+
|
|
282
|
+
// Export individual functions for testing
|
|
283
|
+
module.exports.withOTAUpdateAndroid = withOTAUpdateAndroid;
|
|
284
|
+
module.exports.withOTAUpdateIOS = withOTAUpdateIOS;
|
package/ios/OTAUpdate.m
CHANGED
|
@@ -35,11 +35,20 @@ RCT_EXTERN_METHOD(makeDirectory:(NSString *)path
|
|
|
35
35
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
36
36
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
37
37
|
|
|
38
|
+
RCT_EXTERN_METHOD(downloadFile:(NSString *)urlString
|
|
39
|
+
destPath:(NSString *)destPath
|
|
40
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
41
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
42
|
+
|
|
38
43
|
// Cryptography
|
|
39
44
|
RCT_EXTERN_METHOD(calculateSHA256:(NSString *)base64Content
|
|
40
45
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
41
46
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
42
47
|
|
|
48
|
+
RCT_EXTERN_METHOD(calculateSHA256FromFile:(NSString *)filePath
|
|
49
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
50
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
51
|
+
|
|
43
52
|
RCT_EXTERN_METHOD(verifySignature:(NSString *)base64Content
|
|
44
53
|
signatureHex:(NSString *)signatureHex
|
|
45
54
|
publicKeyHex:(NSString *)publicKeyHex
|