feat: add on-demand ota updates, ota background worker#302
feat: add on-demand ota updates, ota background worker#302PraveenGongada wants to merge 1 commit into
Conversation
WalkthroughThis PR introduces OTA (Over-The-Air) update capabilities to the Airborne SDK and React Native plugin. It adds background update checking and downloading via WorkManager, exposes new update management APIs through the React Native bridge (TypeScript and native layers), implements context management and null-safe config handling in the core SDK, and adds server-side validation for release requests with missing applicable variants. Changes
Sequence DiagramssequenceDiagram
participant App as React Native App
participant Module as AirborneModule
participant Manager as ApplicationManager
participant Server as Update Server
participant Storage as Local Storage
App->>Module: checkForUpdate(namespace)
Module->>Manager: checkForUpdate()
Manager->>Manager: Ensure local config loaded
Manager->>Server: GET latest release config
Server-->>Manager: Release config JSON
Manager->>Manager: Compare versions
Manager-->>Module: Update available JSON
Module-->>App: Promise<string>
App->>Module: downloadUpdate(namespace)
Module->>Manager: downloadUpdate(timeoutMs, callback)
Manager->>Server: Download package
Server-->>Storage: Package bytes
Manager->>Storage: Update index & cache
Manager->>Module: onComplete(success=true)
Module-->>App: Promise<boolean>
sequenceDiagram
participant App as Android System
participant WorkManager as WorkManager
participant Worker as OTADownloadWorker
participant Manager as ApplicationManager
participant Server as Update Server
App->>WorkManager: Schedule background OTA
WorkManager->>Worker: doWork()
Worker->>Worker: Retrieve ApplicationManager from registry
alt Manager not found
Worker->>Worker: Retry (with exponential backoff)
else Manager found
Worker->>Manager: checkForUpdate()
Manager->>Server: Fetch release config
Server-->>Manager: Config JSON
Manager-->>Worker: Update available?
alt Update available
Worker->>Manager: downloadUpdate(callback)
Manager->>Server: Download package
Server-->>Manager: Package bytes
Manager->>Manager: Update local state
Manager->>Worker: onComplete(success)
Worker-->>WorkManager: Result.success()
else No update
Worker-->>WorkManager: Result.success()
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt (1)
100-140:⚠️ Potential issue | 🟠 MajorPreserve
packageTimeoutOverrideinupdateReleaseConfigas well.The new override is respected at initialization and in
updateTimeouts, butupdateReleaseConfigstill resetspackageTimeouttobootTimeout, which can silently ignore caller timeout intent on that path.💡 Proposed fix
fun updateReleaseConfig(newConfig: ReleaseConfig?) { newConfig?.let { localReleaseConfig = it releaseConfigTimeout = it.config.releaseConfigTimeout - packageTimeout = it.config.bootTimeout + packageTimeout = + if (packageTimeoutOverride > 0) packageTimeoutOverride else it.config.bootTimeout // Updating headers too defaultHeaders["x-release-config-version"] = it.version defaultHeaders["x-package-version"] = it.pkg.version defaultHeaders["x-config-version"] = it.config.version } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt` around lines 100 - 140, The updateReleaseConfig function currently sets packageTimeout directly from it.config.bootTimeout and can overwrite a caller-set packageTimeoutOverride; change updateReleaseConfig so when assigning packageTimeout it uses the same logic as updateTimeouts (i.e., packageTimeout = if (packageTimeoutOverride > 0) packageTimeoutOverride else it.config.bootTimeout), leaving releaseConfigTimeout and the header updates unchanged; update references are in the UpdateTask class (method updateReleaseConfig, variables packageTimeoutOverride, packageTimeout).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt`:
- Around line 456-478: checkForUpdate() is racing app boot: otaServices.clientId
may be null because loadApplication() sets it asynchronously and releaseConfig
isn't hydrated from disk, causing spurious errors or operations under an empty
key; fix by first ensuring otaServices.clientId is present (return a clear error
if null or wait/hydrate it) and load the cached release config on-demand before
using fetchLatestReleaseConfig()/releaseConfig, i.e., in checkForUpdate()
validate otaServices.clientId, hydrate releaseConfig from disk if in-memory is
empty, then call fetchLatestReleaseConfig() (or use cached) and proceed; apply
the same guard/hydration pattern to the other methods mentioned
(downloadUpdate(), etc.).
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.kt`:
- Around line 42-56: The worker fails on cold-start because doWork() relies on
an in-memory managerMap[namespace] populated by Airborne.init; update
OTADownloadWorker (doWork / the block using managerMap, downloadWithManager,
runAttemptCount, MAX_RETRIES) to reconstruct or lazily initialize the needed
ApplicationManager when managerMap[namespace] is null instead of immediately
retrying: attempt to reinitialize Airborne or rebuild the specific manager from
persisted configuration (e.g., stored namespace config or preferences), populate
managerMap[namespace], then call downloadWithManager(manager); if reconstruction
is impossible, fail fast with Result.failure() rather than burning retries.
Ensure this logic is applied to both the earlier null-check and the identical
check at lines ~96-97 so background/cold-start runs can proceed without
depending on other in-process initialization.
- Around line 63-73: The worker currently treats any non-available response from
ApplicationManager.checkForUpdate() as "no update" — modify downloadWithManager
to parse checkResult JSON for an "error" field before using "available": if
json.has("error") or json.optString("error").isNotEmpty(), log the error and
return Result.retry() (or appropriate retry behavior) instead of
Result.success(); only return Result.success() when "available" is false and
there is no error. Reference symbols: downloadWithManager,
ApplicationManager.checkForUpdate, checkResult, json, "available", and "error".
In
`@airborne-react-native/android/src/main/java/in/juspay/airborneplugin/Airborne.kt`:
- Around line 99-110: Reintroduce the public setSslConfig API as a stub so
callers get a loud failure instead of losing the method: add back the `@Keep` fun
setSslConfig(sslSocketFactory: SSLSocketFactory, trustManager: X509TrustManager)
in Airborne.kt that checks/uses applicationManager if available and otherwise
throws an explicit UnsupportedOperationException (or IllegalStateException) with
a clear message that ApplicationManager.setSslConfig is not implemented yet;
keep the method signature identical to the commented-out version so clients
compile, reference the same setSslConfig symbol and callsite
(applicationManager.setSslConfig) when implementation becomes available.
In
`@airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.kt`:
- Around line 116-136: In reloadApp, don't resolve the Promise before confirming
the app can actually be relaunched: check
reactContext.applicationContext.packageManager.getLaunchIntentForPackage(context.packageName)
synchronously (or at least before calling promise.resolve) and if it's null call
promise.reject with a descriptive error; only after verifying the intent exists
(and adding flags) resolve the promise or proceed to postDelayed to start the
activity, and if you keep the delayed restart ensure any exceptions in the
Runnable call promise.reject if it hasn't been settled yet; reference reloadApp,
promise.resolve/promise.reject, getLaunchIntentForPackage,
Handler(Looper.getMainLooper()).postDelayed, and
reactContext.currentActivity?.finishAffinity when implementing the change.
- Around line 107-114: startBackgroundDownload currently always resolves true
even when the namespace isn't initialized; mirror the guard used in
checkForUpdate() and downloadUpdate() by verifying the namespace has an
ApplicationManager (or exists in the worker registry) before triggering work—if
the namespace is missing, call promise.reject with the same error code/message
used by checkForUpdate/downloadUpdate instead of resolving, otherwise proceed to
call Airborne.triggerBackgroundDownload and resolve; update
startBackgroundDownload to reference the same lookup/validation logic for
ApplicationManager/worker registry as the other methods.
In `@airborne-react-native/src/NativeAirborne.ts`:
- Around line 22-25: The iOS native module is missing implementations for the
new TurboModule methods declared in NativeAirborne (checkForUpdate,
downloadUpdate, startBackgroundDownload, reloadApp), causing "method not found"
errors on iOS; implement and export matching Objective-C/Swift methods in the
iOS bridge (the class that registers the TurboModule for Airborne) with the same
signatures and promise-based behavior as Android, wire them to the existing
update logic, and ensure the module registration exposes these four methods so
calls to checkForUpdate(nameSpace), downloadUpdate(nameSpace),
startBackgroundDownload(nameSpace), and reloadApp(nameSpace) resolve/reject the
promises consistently with Android.
---
Outside diff comments:
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt`:
- Around line 100-140: The updateReleaseConfig function currently sets
packageTimeout directly from it.config.bootTimeout and can overwrite a
caller-set packageTimeoutOverride; change updateReleaseConfig so when assigning
packageTimeout it uses the same logic as updateTimeouts (i.e., packageTimeout =
if (packageTimeoutOverride > 0) packageTimeoutOverride else
it.config.bootTimeout), leaving releaseConfigTimeout and the header updates
unchanged; update references are in the UpdateTask class (method
updateReleaseConfig, variables packageTimeoutOverride, packageTimeout).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a5fce562-0c0a-4309-9613-b581dab43efb
📒 Files selected for processing (13)
airborne-react-native/android/build.gradleairborne-react-native/android/src/main/java/in/juspay/airborneplugin/Airborne.ktairborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneInterface.ktairborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModule.ktairborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.ktairborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneTurboModule.ktairborne-react-native/src/NativeAirborne.tsairborne-react-native/src/index.tsxairborne_sdk_android/airborne/build.gradleairborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.ktairborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.ktairborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.ktairborne_server/src/release.rs
| fun checkForUpdate(): String { | ||
| try { | ||
| val currentVersion = getCurrentPackageVersion() | ||
|
|
||
| val serverRCString = fetchLatestReleaseConfig() | ||
| ?: return updateCheckResult(currentVersion, error = "Failed to fetch server release config") | ||
|
|
||
| val serverRC = JSONObject(serverRCString) | ||
| val serverVersion = serverRC.getJSONObject("package").getString("version") | ||
| val mandatory = serverRC.getJSONObject("config") | ||
| .optJSONObject("properties") | ||
| ?.optBoolean("mandatory", false) ?: false | ||
|
|
||
| val currentVersionInt = currentVersion.toLongOrNull() ?: 0L | ||
| val serverVersionInt = serverVersion.toLongOrNull() ?: 0L | ||
| val updateAvailable = serverVersionInt > currentVersionInt | ||
|
|
||
| return JSONObject() | ||
| .put("available", updateAvailable) | ||
| .put("currentVersion", currentVersion) | ||
| .put("serverVersion", serverVersion) | ||
| .put("mandatory", mandatory) | ||
| .toString() |
There was a problem hiding this comment.
The on-demand OTA APIs race boot initialization.
Line 89 sets otaServices.clientId inside the loadApplication() background task, but these new methods read it immediately and checkForUpdate() does not hydrate releaseConfig from disk first. A call right after Airborne init can therefore return a synthetic error, compare against an empty/0 current version, or run downloadUpdate() under the shared empty clientId key.
🛠️ Suggested direction
fun checkForUpdate(): String {
try {
+ val rawClientId = otaServices.clientId
+ ?: return updateCheckResult("", error = "ApplicationManager not initialized")
+ val clientId = sanitizeClientId(rawClientId)
+ if (releaseConfig == null) {
+ val (_, contextRef) = ensureContext(clientId)
+ releaseConfig = readReleaseConfig(contextRef)
+ }
val currentVersion = getCurrentPackageVersion()- val clientId = sanitizeClientId(otaServices.clientId ?: "")
+ val rawClientId = otaServices.clientId ?: run {
+ onComplete(false)
+ return@doAsync
+ }
+ val clientId = sanitizeClientId(rawClientId)At minimum, guard the null clientId and load the cached release config on demand here; ideally the clientId assignment should also move out of the async boot task.
Also applies to: 500-523, 533-550
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt`
around lines 456 - 478, checkForUpdate() is racing app boot:
otaServices.clientId may be null because loadApplication() sets it
asynchronously and releaseConfig isn't hydrated from disk, causing spurious
errors or operations under an empty key; fix by first ensuring
otaServices.clientId is present (return a clear error if null or wait/hydrate
it) and load the cached release config on-demand before using
fetchLatestReleaseConfig()/releaseConfig, i.e., in checkForUpdate() validate
otaServices.clientId, hydrate releaseConfig from disk if in-memory is empty,
then call fetchLatestReleaseConfig() (or use cached) and proceed; apply the same
guard/hydration pattern to the other methods mentioned (downloadUpdate(), etc.).
| try { | ||
| val manager = managerMap[namespace] | ||
|
|
||
| if (manager != null) { | ||
| return@withContext downloadWithManager(manager) | ||
| } | ||
|
|
||
| // App was killed — ApplicationManager not initialized. | ||
| // Retry up to MAX_RETRIES times, then give up. | ||
| if (runAttemptCount >= MAX_RETRIES) { | ||
| Log.w(TAG, "ApplicationManager not found for '$namespace' after $MAX_RETRIES attempts. Giving up.") | ||
| return@withContext Result.failure() | ||
| } | ||
| Log.w(TAG, "ApplicationManager not found for '$namespace'. Will retry (attempt $runAttemptCount/$MAX_RETRIES).") | ||
| return@withContext Result.retry() |
There was a problem hiding this comment.
This worker still depends on live process state.
doWork() can only continue when managerMap[namespace] was already populated from Airborne.init, but that registry is in-memory. On a cold-start/background run there is no reconstruction path here, so the job just burns retries unless some other startup code reinitializes Airborne first.
Also applies to: 96-97
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.kt`
around lines 42 - 56, The worker fails on cold-start because doWork() relies on
an in-memory managerMap[namespace] populated by Airborne.init; update
OTADownloadWorker (doWork / the block using managerMap, downloadWithManager,
runAttemptCount, MAX_RETRIES) to reconstruct or lazily initialize the needed
ApplicationManager when managerMap[namespace] is null instead of immediately
retrying: attempt to reinitialize Airborne or rebuild the specific manager from
persisted configuration (e.g., stored namespace config or preferences), populate
managerMap[namespace], then call downloadWithManager(manager); if reconstruction
is impossible, fail fast with Result.failure() rather than burning retries.
Ensure this logic is applied to both the earlier null-check and the identical
check at lines ~96-97 so background/cold-start runs can proceed without
depending on other in-process initialization.
| private suspend fun downloadWithManager(manager: ApplicationManager): Result { | ||
| // Quick version check to avoid heavier download when already up to date | ||
| val checkResult = withContext(Dispatchers.IO) { | ||
| manager.checkForUpdate() | ||
| } | ||
|
|
||
| val json = org.json.JSONObject(checkResult) | ||
| if (!json.optBoolean("available", false)) { | ||
| Log.d(TAG, "No update available, skipping download") | ||
| return Result.success() | ||
| } |
There was a problem hiding this comment.
Retry update-check failures instead of treating them as "no update".
ApplicationManager.checkForUpdate() now reports fetch problems via an error field, but this worker only inspects available. A transient release-config failure will therefore return Result.success() and drop the push-triggered download.
🛠️ Suggested fix
val json = org.json.JSONObject(checkResult)
+ val error = json.optString("error")
+ if (error.isNotEmpty()) {
+ Log.w(TAG, "Update check failed: $error")
+ return Result.retry()
+ }
if (!json.optBoolean("available", false)) {
Log.d(TAG, "No update available, skipping download")
return Result.success()
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.kt`
around lines 63 - 73, The worker currently treats any non-available response
from ApplicationManager.checkForUpdate() as "no update" — modify
downloadWithManager to parse checkResult JSON for an "error" field before using
"available": if json.has("error") or json.optString("error").isNotEmpty(), log
the error and return Result.retry() (or appropriate retry behavior) instead of
Result.success(); only return Result.success() when "available" is false and
there is no error. Reference symbols: downloadWithManager,
ApplicationManager.checkForUpdate, checkResult, json, "available", and "error".
| // TODO: Re-enable once setSslConfig is added back to ApplicationManager | ||
| // /** | ||
| // * Set custom SSL configuration for mTLS support. | ||
| // * Call this before network requests are made to enable client certificate authentication. | ||
| // * | ||
| // * @param sslSocketFactory SSL socket factory configured with client certificate | ||
| // * @param trustManager Trust manager for server certificate validation | ||
| // */ | ||
| // @Keep | ||
| // fun setSslConfig(sslSocketFactory: SSLSocketFactory, trustManager: X509TrustManager) { | ||
| // applicationManager.setSslConfig(sslSocketFactory, trustManager) | ||
| // } |
There was a problem hiding this comment.
Don't replace the public mTLS hook with a TODO.
Commenting out setSslConfig() removes the only obvious API clients have for custom TLS configuration. If ApplicationManager support is not ready yet, keep a stub here that fails loudly instead of silently dropping the method from the surface.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne-react-native/android/src/main/java/in/juspay/airborneplugin/Airborne.kt`
around lines 99 - 110, Reintroduce the public setSslConfig API as a stub so
callers get a loud failure instead of losing the method: add back the `@Keep` fun
setSslConfig(sslSocketFactory: SSLSocketFactory, trustManager: X509TrustManager)
in Airborne.kt that checks/uses applicationManager if available and otherwise
throws an explicit UnsupportedOperationException (or IllegalStateException) with
a clear message that ApplicationManager.setSslConfig is not implemented yet;
keep the method signature identical to the commented-out version so clients
compile, reference the same setSslConfig symbol and callsite
(applicationManager.setSslConfig) when implementation becomes available.
| fun startBackgroundDownload(namespace: String, promise: Promise) { | ||
| try { | ||
| Airborne.triggerBackgroundDownload(reactContext.applicationContext, namespace) | ||
| promise.resolve(true) | ||
| } catch (e: Exception) { | ||
| promise.reject("AIRBORNE_ERROR", "Failed to start background download: ${e.message}", e) | ||
| } | ||
| } |
There was a problem hiding this comment.
Mirror the namespace initialization guard in startBackgroundDownload().
checkForUpdate() and downloadUpdate() reject when the namespace has not been initialized, but this method always resolves true. For an unknown namespace there is no ApplicationManager in the worker registry, so the queued work can only retry/fail.
🛠️ Suggested guard
fun startBackgroundDownload(namespace: String, promise: Promise) {
try {
+ if (in.juspay.airborne.ota.OTADownloadWorker.managerMap[namespace] == null) {
+ promise.reject("AIRBORNE_ERROR", "Airborne not initialized for namespace: $namespace")
+ return
+ }
Airborne.triggerBackgroundDownload(reactContext.applicationContext, namespace)
promise.resolve(true)
} catch (e: Exception) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.kt`
around lines 107 - 114, startBackgroundDownload currently always resolves true
even when the namespace isn't initialized; mirror the guard used in
checkForUpdate() and downloadUpdate() by verifying the namespace has an
ApplicationManager (or exists in the worker registry) before triggering work—if
the namespace is missing, call promise.reject with the same error code/message
used by checkForUpdate/downloadUpdate instead of resolving, otherwise proceed to
call Airborne.triggerBackgroundDownload and resolve; update
startBackgroundDownload to reference the same lookup/validation logic for
ApplicationManager/worker registry as the other methods.
| fun reloadApp(namespace: String, promise: Promise) { | ||
| try { | ||
| promise.resolve(null) | ||
|
|
||
| Handler(Looper.getMainLooper()).postDelayed({ | ||
| try { | ||
| val context = reactContext.applicationContext | ||
| val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) | ||
| if (intent != null) { | ||
| intent.addFlags( | ||
| android.content.Intent.FLAG_ACTIVITY_NEW_TASK or | ||
| android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK | ||
| ) | ||
| context.startActivity(intent) | ||
| reactContext.currentActivity?.finishAffinity() | ||
| android.os.Process.killProcess(android.os.Process.myPid()) | ||
| } | ||
| } catch (e: Exception) { | ||
| android.util.Log.e("AirborneModuleImpl", "Failed to reload app", e) | ||
| } | ||
| }, 200) |
There was a problem hiding this comment.
Validate the relaunch path before resolving the promise.
The method resolves immediately and only then looks up the launch intent / starts the activity inside the delayed runnable, so JS gets a successful result even when restart is impossible and the failure is only logged. Either precompute that path and reject early, or make this a fire-and-forget API instead of a resolved promise.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.kt`
around lines 116 - 136, In reloadApp, don't resolve the Promise before
confirming the app can actually be relaunched: check
reactContext.applicationContext.packageManager.getLaunchIntentForPackage(context.packageName)
synchronously (or at least before calling promise.resolve) and if it's null call
promise.reject with a descriptive error; only after verifying the intent exists
(and adding flags) resolve the promise or proceed to postDelayed to start the
activity, and if you keep the delayed restart ensure any exceptions in the
Runnable call promise.reject if it hasn't been settled yet; reference reloadApp,
promise.resolve/promise.reject, getLaunchIntentForPackage,
Handler(Looper.getMainLooper()).postDelayed, and
reactContext.currentActivity?.finishAffinity when implementing the change.
| checkForUpdate(nameSpace: string): Promise<string>; | ||
| downloadUpdate(nameSpace: string): Promise<boolean>; | ||
| startBackgroundDownload(nameSpace: string): Promise<boolean>; | ||
| reloadApp(nameSpace: string): Promise<void>; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== iOS Airborne bridge files =="
fd -i 'Airborne*' airborne-react-native/ios || true
echo
echo "== Search for newly added APIs on iOS =="
rg -n -C2 'checkForUpdate|downloadUpdate|startBackgroundDownload|reloadApp' airborne-react-native/ios || true
echo
echo "== Search for ObjC/Swift exported RN methods =="
rg -n -C2 'RCT_EXPORT_METHOD|RCT_REMAP_METHOD|@objc' airborne-react-native/ios || trueRepository: juspay/airborne
Length of output: 1497
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Android Airborne bridge files =="
fd -i 'Airborne.*\.kt|Airborne.*\.java' airborne-react-native/android || true
echo
echo "== Search for newly added APIs in Android =="
rg -n 'checkForUpdate|downloadUpdate|startBackgroundDownload|reloadApp' airborne-react-native/android || true
echo
echo "== Count exported methods in Android vs iOS =="
echo "Android exports:"
rg '@ReactMethod' airborne-react-native/android | wc -l || echo "0"
echo "iOS exports:"
rg 'RCT_EXPORT_METHOD' airborne-react-native/ios | wc -l || echo "0"Repository: juspay/airborne
Length of output: 5626
iOS bridge missing new TurboModule methods.
These four methods are now mandatory in the shared spec (lines 22-25), but iOS exports none of them. Android has full implementation. Consumers will hit "method not found" runtime errors when calling checkForUpdate(), downloadUpdate(), startBackgroundDownload(), or reloadApp() on iOS.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@airborne-react-native/src/NativeAirborne.ts` around lines 22 - 25, The iOS
native module is missing implementations for the new TurboModule methods
declared in NativeAirborne (checkForUpdate, downloadUpdate,
startBackgroundDownload, reloadApp), causing "method not found" errors on iOS;
implement and export matching Objective-C/Swift methods in the iOS bridge (the
class that registers the TurboModule for Airborne) with the same signatures and
promise-based behavior as Android, wire them to the existing update logic, and
ensure the module registration exposes these four methods so calls to
checkForUpdate(nameSpace), downloadUpdate(nameSpace),
startBackgroundDownload(nameSpace), and reloadApp(nameSpace) resolve/reject the
promises consistently with Android.
Summary by CodeRabbit
New Features
Bug Fixes