Skip to content

feat: add on-demand ota updates, ota background worker#302

Open
PraveenGongada wants to merge 1 commit into
juspay:mainfrom
PraveenGongada:feat/on-demand-ota-updates
Open

feat: add on-demand ota updates, ota background worker#302
PraveenGongada wants to merge 1 commit into
juspay:mainfrom
PraveenGongada:feat/on-demand-ota-updates

Conversation

@PraveenGongada
Copy link
Copy Markdown

@PraveenGongada PraveenGongada commented Apr 8, 2026

  • feat: add on-demand OTA update methods — checkForUpdate, downloadUpdate, startBackgroundDownload, reloadApp
  • feat: add OTADownloadWorker for FCM-triggered background bundle downloads via WorkManager
  • fix: resolve bundle version mismatches when dimensions are provided but no matching release exists (server now returns 404)

Summary by CodeRabbit

  • New Features

    • Added over-the-air (OTA) update checking to detect available app updates
    • Added on-demand update downloading with completion callbacks
    • Added background update download support for seamless updates
    • Added app reload functionality after updates are applied
  • Bug Fixes

    • Fixed error handling when no applicable release matches requested dimensions

@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented Apr 8, 2026

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Walkthrough

This 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

Cohort / File(s) Summary
OTA Update Infrastructure
airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt, airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.kt, airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt
New OTADownloadWorker CoroutineWorker for background downloads with retry logic and WorkManager scheduling. ApplicationManager adds checkForUpdate(), downloadUpdate(), fetchLatestReleaseConfig(), getCurrentPackageVersion() APIs, context management via ensureContext(), null-safe release config loading, and package timeout override support. UpdateTask accepts packageTimeoutOverride parameter.
Core Plugin Implementation
airborne-react-native/android/src/main/java/in/juspay/airborneplugin/Airborne.kt, airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneInterface.kt
Airborne migrated to ConcurrentHashMap for thread-safety, now registers OTA worker state, replaced setSslConfig() with checkForUpdate() and downloadUpdate() methods, added static triggerBackgroundDownload(). AirborneInterface adds optional enableBootDownload() hook returning default true.
React Native Bridge Layer
airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModule.kt, airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.kt, airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneTurboModule.kt
Four new React Native bridge methods (checkForUpdate, downloadUpdate, startBackgroundDownload, reloadApp) added to AirborneModule. AirborneModuleImpl provides implementations with proper threading (background thread for checks, main thread for callbacks), promise handling, and app reload logic with delayed activity restart. AirborneTurboModule overrides all four methods to forward calls.
TypeScript API Layer
airborne-react-native/src/NativeAirborne.ts, airborne-react-native/src/index.tsx
Spec interface extended with four update methods returning appropriate promise types. Wrapper functions exported from main module that delegate to Airborne native module.
Build Configuration
airborne-react-native/android/build.gradle, airborne_sdk_android/airborne/build.gradle
Minor whitespace adjustment in react-native plugin. SDK adds androidx.work:work-runtime-ktx:2.9.1 and kotlinx-coroutines-android:1.7.3 dependencies for WorkManager and coroutines support.
Server Validation
airborne_server/src/release.rs
Added early failure check in serve_release_handler to return NotFound when request context is non-empty but no applicable variants exist.

Sequence Diagrams

sequenceDiagram
    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>
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

enhancement, javascript, kotlin

Suggested reviewers

  • JamesGeorg
  • yuvrajjsingh0

Poem

🐰 Updates hop in from the sky so bright,
Background workers toil through the night,
Version checks dance with promises true,
WorkManager queues bring updates brand new!
Apps reload with a flourish and grace,
OTA brings progress to every place. 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main changes: adding on-demand OTA update functionality and an OTA background worker for scheduled bundle downloads.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Preserve packageTimeoutOverride in updateReleaseConfig as well.

The new override is respected at initialization and in updateTimeouts, but updateReleaseConfig still resets packageTimeout to bootTimeout, 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

📥 Commits

Reviewing files that changed from the base of the PR and between fd3e886 and 2b6579e.

📒 Files selected for processing (13)
  • airborne-react-native/android/build.gradle
  • airborne-react-native/android/src/main/java/in/juspay/airborneplugin/Airborne.kt
  • airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneInterface.kt
  • airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModule.kt
  • airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneModuleImpl.kt
  • airborne-react-native/android/src/main/java/in/juspay/airborneplugin/AirborneTurboModule.kt
  • airborne-react-native/src/NativeAirborne.ts
  • airborne-react-native/src/index.tsx
  • airborne_sdk_android/airborne/build.gradle
  • airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt
  • airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/OTADownloadWorker.kt
  • airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt
  • airborne_server/src/release.rs

Comment on lines +456 to +478
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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.).

Comment on lines +42 to +56
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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +63 to +73
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()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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".

Comment on lines +99 to +110
// 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)
// }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +107 to +114
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)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +116 to +136
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +22 to +25
checkForUpdate(nameSpace: string): Promise<string>;
downloadUpdate(nameSpace: string): Promise<boolean>;
startBackgroundDownload(nameSpace: string): Promise<boolean>;
reloadApp(nameSpace: string): Promise<void>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 || true

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant