@vanikya/ota-react-native 0.1.9 → 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.
Files changed (3) hide show
  1. package/README.md +135 -1
  2. package/app.plugin.js +201 -21
  3. package/package.json +1 -1
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
package/app.plugin.js CHANGED
@@ -1,6 +1,15 @@
1
- const { withMainApplication, withAppDelegate, withDangerousMod } = require('@expo/config-plugins');
2
- const fs = require('fs');
3
- const path = require('path');
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
- /^(package .+?\n)/m,
18
- `$1\nimport android.content.SharedPreferences\nimport java.io.File\n`
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
- // For Kotlin-based MainApplication (Expo SDK 50+)
23
- // Find the ReactNativeHost and add getJSBundleFile override
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 (kotlinPattern.test(contents)) {
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
- kotlinPattern,
29
- `override fun getJSBundleFile(): String? {
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
- override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG`
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
- // Add helper function before the class closing brace
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 defaults = UserDefaults.standard
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
- // Find bundleURL method and modify it
74
- const bundleURLPattern = /func\s+bundleURL\(\)\s*->\s*URL\?\s*\{[\s\S]*?\n\s*\}/;
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 (bundleURLPattern.test(contents)) {
193
+ if (sourceURLPattern.test(contents)) {
77
194
  contents = contents.replace(
78
- bundleURLPattern,
79
- `func bundleURL() -> URL? {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanikya/ota-react-native",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "OTA Update SDK for React Native apps - self-hosted CodePush/EAS Updates alternative",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",