@vanikya/ota-react-native 0.2.4 → 0.2.6

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.
@@ -49,4 +49,6 @@ repositories {
49
49
  dependencies {
50
50
  //noinspection GradleDynamicVersion
51
51
  implementation "com.facebook.react:react-android:+"
52
+ // ProcessPhoenix for graceful app restart
53
+ implementation "com.jakewharton:process-phoenix:3.0.0"
52
54
  }
@@ -2,17 +2,22 @@ package com.otaupdate
2
2
 
3
3
  import android.content.Context
4
4
  import android.content.SharedPreferences
5
+ import android.content.pm.PackageInfo
6
+ import android.os.Build
5
7
  import android.util.Log
6
8
  import java.io.File
7
9
 
8
10
  /**
9
11
  * Helper object for OTA Update bundle loading.
10
12
  * This is called from MainApplication.getJSBundleFile() to get the OTA bundle path.
13
+ *
14
+ * Based on patterns from react-native-ota-hot-update and hot-updater libraries.
11
15
  */
12
16
  object OTAUpdateHelper {
13
17
  private const val TAG = "OTAUpdate"
14
18
  private const val PREFS_NAME = "OTAUpdate"
15
19
  private const val KEY_BUNDLE_PATH = "BundlePath"
20
+ private const val KEY_APP_VERSION = "AppVersion"
16
21
 
17
22
  /**
18
23
  * Get the JS bundle file path for React Native to load.
@@ -24,10 +29,25 @@ object OTAUpdateHelper {
24
29
  @JvmStatic
25
30
  fun getJSBundleFile(context: Context): String? {
26
31
  return try {
32
+ Log.d(TAG, "=== OTAUpdateHelper.getJSBundleFile called ===")
33
+
27
34
  val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
28
35
  val bundlePath = prefs.getString(KEY_BUNDLE_PATH, null)
36
+ val savedAppVersion = prefs.getString(KEY_APP_VERSION, null)
37
+ val currentAppVersion = getAppVersionCode(context)
38
+
39
+ Log.d(TAG, "Stored bundle path: $bundlePath")
40
+ Log.d(TAG, "Saved app version: $savedAppVersion, Current app version: $currentAppVersion")
29
41
 
30
- Log.d(TAG, "getJSBundleFile called, stored path: $bundlePath")
42
+ // Clear bundle if app version changed (user updated the app)
43
+ if (savedAppVersion != null && savedAppVersion != currentAppVersion.toString()) {
44
+ Log.w(TAG, "App version changed from $savedAppVersion to $currentAppVersion, clearing OTA bundle")
45
+ prefs.edit()
46
+ .remove(KEY_BUNDLE_PATH)
47
+ .remove(KEY_APP_VERSION)
48
+ .commit()
49
+ return null
50
+ }
31
51
 
32
52
  if (bundlePath.isNullOrEmpty()) {
33
53
  Log.d(TAG, "No OTA bundle path stored, loading default bundle")
@@ -35,6 +55,9 @@ object OTAUpdateHelper {
35
55
  }
36
56
 
37
57
  val file = File(bundlePath)
58
+ Log.d(TAG, "Checking file: ${file.absolutePath}")
59
+ Log.d(TAG, "File exists: ${file.exists()}, canRead: ${file.canRead()}")
60
+
38
61
  if (!file.exists()) {
39
62
  Log.w(TAG, "OTA bundle file does not exist: $bundlePath")
40
63
  // Clear invalid path
@@ -44,6 +67,7 @@ object OTAUpdateHelper {
44
67
 
45
68
  if (!file.canRead()) {
46
69
  Log.w(TAG, "OTA bundle file is not readable: $bundlePath")
70
+ prefs.edit().remove(KEY_BUNDLE_PATH).commit()
47
71
  return null
48
72
  }
49
73
 
@@ -54,7 +78,23 @@ object OTAUpdateHelper {
54
78
  return null
55
79
  }
56
80
 
57
- Log.d(TAG, "Loading OTA bundle: $bundlePath ($fileSize bytes)")
81
+ // Verify it looks like a JS file by checking first bytes
82
+ try {
83
+ val firstBytes = file.inputStream().use { it.readNBytes(50) }
84
+ val preview = String(firstBytes, Charsets.UTF_8)
85
+ Log.d(TAG, "Bundle preview: ${preview.take(30)}...")
86
+
87
+ // Check for HTML (error pages)
88
+ if (preview.contains("<!DOCTYPE") || preview.contains("<html") || preview.contains("<HTML")) {
89
+ Log.e(TAG, "OTA bundle appears to be HTML, not JavaScript - clearing")
90
+ prefs.edit().remove(KEY_BUNDLE_PATH).commit()
91
+ return null
92
+ }
93
+ } catch (e: Exception) {
94
+ Log.w(TAG, "Could not verify bundle content: ${e.message}")
95
+ }
96
+
97
+ Log.d(TAG, "✓ Loading OTA bundle: $bundlePath ($fileSize bytes)")
58
98
  bundlePath
59
99
  } catch (e: Exception) {
60
100
  Log.e(TAG, "Error getting JS bundle file: ${e.message}", e)
@@ -62,6 +102,31 @@ object OTAUpdateHelper {
62
102
  }
63
103
  }
64
104
 
105
+ /**
106
+ * Get app version code for cache invalidation
107
+ */
108
+ @JvmStatic
109
+ fun getAppVersionCode(context: Context): Long {
110
+ return try {
111
+ val packageInfo: PackageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
112
+ context.packageManager.getPackageInfo(context.packageName, android.content.pm.PackageManager.PackageInfoFlags.of(0))
113
+ } else {
114
+ @Suppress("DEPRECATION")
115
+ context.packageManager.getPackageInfo(context.packageName, 0)
116
+ }
117
+
118
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
119
+ packageInfo.longVersionCode
120
+ } else {
121
+ @Suppress("DEPRECATION")
122
+ packageInfo.versionCode.toLong()
123
+ }
124
+ } catch (e: Exception) {
125
+ Log.e(TAG, "Error getting app version: ${e.message}")
126
+ 0L
127
+ }
128
+ }
129
+
65
130
  /**
66
131
  * Check if an OTA bundle is pending to be loaded.
67
132
  *
@@ -84,7 +149,24 @@ object OTAUpdateHelper {
84
149
  @JvmStatic
85
150
  fun clearPendingBundle(context: Context) {
86
151
  val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
87
- prefs.edit().remove(KEY_BUNDLE_PATH).commit()
152
+ prefs.edit()
153
+ .remove(KEY_BUNDLE_PATH)
154
+ .remove(KEY_APP_VERSION)
155
+ .commit()
88
156
  Log.d(TAG, "Pending bundle cleared")
89
157
  }
158
+
159
+ /**
160
+ * Save the bundle path with app version for cache invalidation
161
+ */
162
+ @JvmStatic
163
+ fun saveBundlePath(context: Context, bundlePath: String) {
164
+ val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
165
+ val appVersion = getAppVersionCode(context)
166
+ prefs.edit()
167
+ .putString(KEY_BUNDLE_PATH, bundlePath)
168
+ .putString(KEY_APP_VERSION, appVersion.toString())
169
+ .commit()
170
+ Log.d(TAG, "Bundle path saved: $bundlePath (app version: $appVersion)")
171
+ }
90
172
  }
@@ -6,6 +6,7 @@ import android.os.Handler
6
6
  import android.os.Looper
7
7
  import android.util.Base64
8
8
  import com.facebook.react.bridge.*
9
+ import com.jakewharton.processphoenix.ProcessPhoenix
9
10
  import java.io.File
10
11
  import java.io.FileOutputStream
11
12
  import java.io.InputStream
@@ -303,26 +304,35 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
303
304
  }
304
305
 
305
306
  if (restart) {
306
- android.util.Log.d("OTAUpdate", "Restarting app to apply bundle...")
307
+ android.util.Log.d("OTAUpdate", "Restarting app to apply bundle using ProcessPhoenix...")
307
308
 
308
309
  // Resolve promise before restarting so JS knows it succeeded
309
310
  promise.resolve(null)
310
311
 
311
312
  // Give a small delay to ensure the promise is sent back to JS
312
313
  mainHandler.postDelayed({
313
- // Restart the app
314
- val context = reactApplicationContext
315
- val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
316
- intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
317
- intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
318
- intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
319
- context.startActivity(intent)
320
-
321
- // Small delay before killing to allow activity to start
322
- mainHandler.postDelayed({
314
+ try {
315
+ // Use ProcessPhoenix for graceful app restart
316
+ // This properly kills the current process and starts a new one
317
+ val activity = currentActivity
318
+ if (activity != null) {
319
+ ProcessPhoenix.triggerRebirth(activity)
320
+ } else {
321
+ // Fallback to context-based restart
322
+ ProcessPhoenix.triggerRebirth(reactApplicationContext)
323
+ }
324
+ } catch (e: Exception) {
325
+ android.util.Log.e("OTAUpdate", "ProcessPhoenix restart failed: ${e.message}, using fallback")
326
+ // Fallback to manual restart
327
+ val context = reactApplicationContext
328
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
329
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
330
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
331
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
332
+ context.startActivity(intent)
323
333
  android.os.Process.killProcess(android.os.Process.myPid())
324
- }, 100)
325
- }, 100)
334
+ }
335
+ }, 200)
326
336
  } else {
327
337
  promise.resolve(null)
328
338
  }
@@ -345,6 +355,24 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
345
355
  promise.resolve(null)
346
356
  }
347
357
 
358
+ // Reload the app - used to apply pending updates
359
+ @ReactMethod
360
+ fun reload() {
361
+ android.util.Log.d("OTAUpdate", "Reload requested...")
362
+ mainHandler.postDelayed({
363
+ try {
364
+ val activity = currentActivity
365
+ if (activity != null) {
366
+ ProcessPhoenix.triggerRebirth(activity)
367
+ } else {
368
+ ProcessPhoenix.triggerRebirth(reactApplicationContext)
369
+ }
370
+ } catch (e: Exception) {
371
+ android.util.Log.e("OTAUpdate", "Reload failed: ${e.message}")
372
+ }
373
+ }, 100)
374
+ }
375
+
348
376
  // Utility functions
349
377
 
350
378
  private fun hexStringToByteArray(hex: String): ByteArray {
package/app.plugin.js CHANGED
@@ -4,11 +4,11 @@ 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 using OTAUpdateHelper
7
+ * - Android: Overrides getJSBundleFile() or sets jsBundleFilePath in MainApplication.kt
8
8
  * - iOS: Modifies bundleURL() in AppDelegate.swift
9
9
  *
10
- * The modifications check SharedPreferences (Android) / UserDefaults (iOS)
11
- * for a pending OTA bundle path and load it instead of the default bundle.
10
+ * Supports both old architecture (getJSBundleFile override) and
11
+ * new architecture (jsBundleFilePath parameter in getDefaultReactHost).
12
12
  */
13
13
 
14
14
  function withOTAUpdateAndroid(config) {
@@ -16,122 +16,152 @@ function withOTAUpdateAndroid(config) {
16
16
  let contents = config.modResults.contents;
17
17
 
18
18
  // Check if already modified
19
- if (contents.includes('OTAUpdateHelper')) {
19
+ if (contents.includes('OTAUpdateHelper') || contents.includes('com.otaupdate')) {
20
20
  console.log('[OTAUpdate] Android: OTAUpdateHelper already present, skipping');
21
21
  return config;
22
22
  }
23
23
 
24
- // Add import for OTAUpdateHelper
24
+ console.log('[OTAUpdate] Android: Analyzing MainApplication.kt structure...');
25
+
26
+ // Add import for OTAUpdateHelper at the top
25
27
  const packageMatch = contents.match(/^package\s+[\w.]+\s*\n/m);
26
28
  if (packageMatch) {
27
29
  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);
30
+ const importStatement = `\nimport com.otaupdate.OTAUpdateHelper\n`;
31
+ contents = contents.slice(0, insertPos) + importStatement + contents.slice(insertPos);
32
+ console.log('[OTAUpdate] Android: Added import for OTAUpdateHelper');
33
+ }
34
+
35
+ let injected = false;
36
+
37
+ // ============================================================
38
+ // Strategy 1: React Native 0.82+ NEW ARCHITECTURE
39
+ // Look for: getDefaultReactHost(applicationContext, ...) and add jsBundleFilePath parameter
40
+ // ============================================================
41
+ const newArchPattern = /getDefaultReactHost\s*\(\s*\n?\s*applicationContext\s*,/;
42
+ if (newArchPattern.test(contents)) {
43
+ console.log('[OTAUpdate] Android: Detected React Native 0.82+ (new architecture)');
44
+
45
+ // Find the getDefaultReactHost call and add jsBundleFilePath parameter
46
+ // Pattern: getDefaultReactHost(applicationContext, packageList, ...)
47
+ const reactHostRegex = /(getDefaultReactHost\s*\(\s*\n?\s*)(applicationContext)(\s*,)/g;
48
+
49
+ if (reactHostRegex.test(contents)) {
50
+ contents = contents.replace(
51
+ /(getDefaultReactHost\s*\(\s*\n?\s*)(applicationContext)(\s*,)/,
52
+ '$1$2$3\n jsBundleFilePath = OTAUpdateHelper.getJSBundleFile(applicationContext),'
53
+ );
54
+ console.log('[OTAUpdate] Android: Injected jsBundleFilePath parameter (new architecture)');
55
+ injected = true;
32
56
  }
33
57
  }
34
58
 
35
- // The getJSBundleFile override that uses our helper
36
- const getJSBundleFileOverride = `
37
- override fun getJSBundleFile(): String? {
38
- return OTAUpdateHelper.getJSBundleFile(applicationContext)
59
+ // ============================================================
60
+ // Strategy 2: Look for reactHost with getDefaultReactHost
61
+ // override val reactHost: ReactHost get() = getDefaultReactHost(...)
62
+ // ============================================================
63
+ if (!injected) {
64
+ const reactHostGetPattern = /(override\s+val\s+reactHost\s*:\s*ReactHost\s+get\s*\(\s*\)\s*=\s*getDefaultReactHost\s*\()([^)]*)\)/;
65
+ if (reactHostGetPattern.test(contents)) {
66
+ console.log('[OTAUpdate] Android: Detected reactHost with getDefaultReactHost');
67
+
68
+ contents = contents.replace(
69
+ reactHostGetPattern,
70
+ (match, prefix, params) => {
71
+ if (params.includes('jsBundleFilePath')) {
72
+ return match; // Already has it
73
+ }
74
+ // Add jsBundleFilePath to the parameters
75
+ const newParams = params.trim() + ',\n jsBundleFilePath = OTAUpdateHelper.getJSBundleFile(applicationContext)';
76
+ return `${prefix}${newParams})`;
77
+ }
78
+ );
79
+ console.log('[OTAUpdate] Android: Injected jsBundleFilePath in reactHost getter');
80
+ injected = true;
39
81
  }
40
- `;
82
+ }
41
83
 
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;
84
+ // ============================================================
85
+ // Strategy 3: Old architecture - DefaultReactNativeHost with object block
86
+ // Look for: object : DefaultReactNativeHost(this) { ... }
87
+ // ============================================================
88
+ if (!injected) {
89
+ const defaultHostPattern = /(object\s*:\s*DefaultReactNativeHost\s*\([^)]*\)\s*\{)/;
90
+ if (defaultHostPattern.test(contents)) {
91
+ console.log('[OTAUpdate] Android: Detected DefaultReactNativeHost (old architecture)');
92
+
93
+ const getJSBundleFileOverride = `
94
+ override fun getJSBundleFile(): String? {
95
+ return OTAUpdateHelper.getJSBundleFile(applicationContext)
96
+ }
97
+ `;
98
+ contents = contents.replace(defaultHostPattern, `$1${getJSBundleFileOverride}`);
99
+ console.log('[OTAUpdate] Android: Injected getJSBundleFile override');
100
+ injected = true;
101
+ }
54
102
  }
55
103
 
56
- // Strategy 2: Look for "override val reactNativeHost" with object block
57
- const reactNativeHostPattern = /(override\s+val\s+reactNativeHost[^=]*=\s*object[^{]*\{)/;
104
+ // ============================================================
105
+ // Strategy 4: Look for override fun getUseDeveloperSupport and insert before it
106
+ // ============================================================
107
+ if (!injected) {
108
+ const devSupportPattern = /([ \t]*)(override\s+fun\s+getUseDeveloperSupport\s*\(\s*\))/;
109
+ if (devSupportPattern.test(contents)) {
110
+ console.log('[OTAUpdate] Android: Found getUseDeveloperSupport, inserting before it');
58
111
 
59
- if (reactNativeHostPattern.test(contents)) {
60
- contents = contents.replace(
61
- reactNativeHostPattern,
62
- `$1${getJSBundleFileOverride}`
63
- );
64
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (reactNativeHost pattern)');
65
- config.modResults.contents = contents;
66
- return config;
67
- }
112
+ const match = contents.match(devSupportPattern);
113
+ const indent = match ? match[1] : ' ';
68
114
 
69
- // Strategy 3: Look for any ReactNativeHost object block
70
- const anyHostPattern = /(ReactNativeHost\s*\([^)]*\)\s*\{)/;
115
+ const getJSBundleFileOverride = `${indent}override fun getJSBundleFile(): String? {
116
+ ${indent} return OTAUpdateHelper.getJSBundleFile(applicationContext)
117
+ ${indent}}
71
118
 
72
- if (anyHostPattern.test(contents)) {
73
- contents = contents.replace(
74
- anyHostPattern,
75
- `$1${getJSBundleFileOverride}`
76
- );
77
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (generic ReactNativeHost pattern)');
78
- config.modResults.contents = contents;
79
- return config;
80
- }
119
+ ${indent}`;
81
120
 
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)/;
85
-
86
- if (devSupportPattern.test(contents)) {
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}`;
90
- contents = contents.replace(
91
- devSupportPattern,
92
- `${overrideCode}$2`
93
- );
94
- console.log('[OTAUpdate] Android: Successfully injected getJSBundleFile (before getUseDeveloperSupport)');
95
- config.modResults.contents = contents;
96
- return config;
121
+ contents = contents.replace(devSupportPattern, `${getJSBundleFileOverride}$2`);
122
+ console.log('[OTAUpdate] Android: Injected getJSBundleFile before getUseDeveloperSupport');
123
+ injected = true;
124
+ }
97
125
  }
98
126
 
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*\{)/;
102
-
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
- }
127
+ // ============================================================
128
+ // Strategy 5: Look for ReactNativeHost in any form
129
+ // ============================================================
130
+ if (!injected) {
131
+ const anyHostPattern = /(:\s*ReactNativeHost\s*\{)/;
132
+ if (anyHostPattern.test(contents)) {
133
+ console.log('[OTAUpdate] Android: Found ReactNativeHost block');
134
+
135
+ const getJSBundleFileOverride = `
136
+ override fun getJSBundleFile(): String? {
137
+ return OTAUpdateHelper.getJSBundleFile(applicationContext)
138
+ }
113
139
  `;
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');
140
+ contents = contents.replace(anyHostPattern, `$1${getJSBundleFileOverride}`);
141
+ console.log('[OTAUpdate] Android: Injected getJSBundleFile in ReactNativeHost');
142
+ injected = true;
117
143
  }
118
144
  }
119
145
 
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');
124
- console.warn('[OTAUpdate] Android: See https://vanikya.github.io/ota-update/ for manual setup instructions');
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
- });
146
+ if (!injected) {
147
+ console.warn('[OTAUpdate] Android: ⚠️ Could not find injection point!');
148
+ console.warn('[OTAUpdate] Android: Please manually add getJSBundleFile override');
149
+ console.warn('[OTAUpdate] Android: See documentation for manual setup');
150
+
151
+ // Log relevant lines for debugging
152
+ const lines = contents.split('\n');
153
+ console.log('[OTAUpdate] Android: Relevant lines in MainApplication.kt:');
154
+ lines.forEach((line, i) => {
155
+ if (line.includes('ReactNativeHost') ||
156
+ line.includes('DefaultReactNativeHost') ||
157
+ line.includes('getDefaultReactHost') ||
158
+ line.includes('reactHost') ||
159
+ line.includes('getJSBundleFile') ||
160
+ line.includes('jsBundleFilePath')) {
161
+ console.log(` ${i + 1}: ${line.trim()}`);
162
+ }
163
+ });
164
+ }
135
165
 
136
166
  config.modResults.contents = contents;
137
167
  return config;
@@ -165,8 +195,7 @@ function withOTAUpdateIOS(config) {
165
195
  }
166
196
  return URL(fileURLWithPath: path)
167
197
  } else {
168
- NSLog("[OTAUpdate] OTA bundle not found at path: %@", path)
169
- // Clear invalid path
198
+ NSLog("[OTAUpdate] OTA bundle not found at path: %@, clearing", path)
170
199
  UserDefaults.standard.removeObject(forKey: "OTAUpdateBundlePath")
171
200
  }
172
201
  }
@@ -193,12 +222,12 @@ function withOTAUpdateIOS(config) {
193
222
  ${funcBody}${funcEnd}${helperFunction}`;
194
223
  }
195
224
  );
196
- console.log('[OTAUpdate] iOS: Successfully modified bundleURL (pattern 1)');
225
+ console.log('[OTAUpdate] iOS: Successfully modified bundleURL');
197
226
  config.modResults.contents = contents;
198
227
  return config;
199
228
  }
200
229
 
201
- // Strategy 2: Look for sourceURL(for bridge:) pattern (older Expo versions)
230
+ // Strategy 2: Look for sourceURL(for bridge:) pattern
202
231
  const sourceURLPattern = /(func\s+sourceURL\s*\(\s*for\s+bridge\s*:\s*RCTBridge\s*\)\s*->\s*URL\?\s*\{)([\s\S]*?)(\n\s*\})/;
203
232
 
204
233
  if (sourceURLPattern.test(contents)) {
@@ -216,49 +245,19 @@ ${funcBody}${funcEnd}${helperFunction}`;
216
245
  ${funcBody}${funcEnd}${helperFunction}`;
217
246
  }
218
247
  );
219
- console.log('[OTAUpdate] iOS: Successfully modified sourceURL (pattern 2)');
220
- config.modResults.contents = contents;
221
- return config;
222
- }
223
-
224
- // Strategy 3: Add bundleURL method if it doesn't exist but class exists
225
- const appDelegateClassPattern = /(class\s+AppDelegate\s*[^{]*\{)/;
226
-
227
- if (appDelegateClassPattern.test(contents) && !contents.includes('func bundleURL')) {
228
- const newBundleURLMethod = `
229
- // OTA Update: Bundle URL with OTA support
230
- func bundleURL() -> URL? {
231
- // Check for downloaded OTA bundle first
232
- if let otaBundle = getOTABundleURL() {
233
- return otaBundle
234
- }
235
- #if DEBUG
236
- return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
237
- #else
238
- return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
239
- #endif
240
- }
241
- ${helperFunction}
242
- `;
243
- contents = contents.replace(
244
- appDelegateClassPattern,
245
- `$1${newBundleURLMethod}`
246
- );
247
- console.log('[OTAUpdate] iOS: Successfully added bundleURL method (pattern 3)');
248
+ console.log('[OTAUpdate] iOS: Successfully modified sourceURL');
248
249
  config.modResults.contents = contents;
249
250
  return config;
250
251
  }
251
252
 
252
- console.warn('[OTAUpdate] iOS: Could not find suitable injection point');
253
- console.warn('[OTAUpdate] iOS: Please manually modify AppDelegate.swift');
254
- console.warn('[OTAUpdate] iOS: See https://vanikya.github.io/ota-update/ for manual setup instructions');
253
+ console.warn('[OTAUpdate] iOS: Could not find bundleURL or sourceURL method');
255
254
  } else if (config.modResults.language === 'objc' || config.modResults.language === 'objcpp') {
256
- // Objective-C AppDelegate (older Expo versions)
255
+ // Objective-C AppDelegate
257
256
  const sourceURLPattern = /(- \(NSURL \*\)sourceURLForBridge:\(RCTBridge \*\)bridge\s*\{)([\s\S]*?)(\n\})/;
258
257
 
259
258
  if (sourceURLPattern.test(contents)) {
260
259
  const helperCode = `
261
- // Check for OTA bundle
260
+ // OTA Update: Check for OTA bundle
262
261
  NSString *bundlePath = [[NSUserDefaults standardUserDefaults] stringForKey:@"OTAUpdateBundlePath"];
263
262
  if (bundlePath && [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
264
263
  NSLog(@"[OTAUpdate] Loading OTA bundle: %@", bundlePath);
@@ -292,6 +291,5 @@ module.exports = function withOTAUpdate(config) {
292
291
  return config;
293
292
  };
294
293
 
295
- // Export individual functions for testing
296
294
  module.exports.withOTAUpdateAndroid = withOTAUpdateAndroid;
297
295
  module.exports.withOTAUpdateIOS = withOTAUpdateIOS;
@@ -103,5 +103,5 @@ var _verification = require("./utils/verification");
103
103
  // Utilities
104
104
 
105
105
  // Version info
106
- const VERSION = exports.VERSION = '0.2.4';
106
+ const VERSION = exports.VERSION = '0.2.6';
107
107
  //# sourceMappingURL=index.js.map
@@ -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.4';
13
+ export const VERSION = '0.2.6';
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.4";
12
+ export declare const VERSION = "0.2.6";
13
13
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanikya/ota-react-native",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
package/src/index.ts CHANGED
@@ -36,4 +36,4 @@ export {
36
36
  export type { VerificationResult } from './utils/verification';
37
37
 
38
38
  // Version info
39
- export const VERSION = '0.2.4';
39
+ export const VERSION = '0.2.6';