Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions airborne-react-native/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ dependencies {

// Airborne SDK dependency
api "in.juspay:airborne:2.2.7-xota.02"

}

react {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import `in`.juspay.airborne.LazyDownloadCallback
import `in`.juspay.airborne.TrackerCallback
import `in`.juspay.hyperutil.constants.LogLevel
import org.json.JSONObject
import `in`.juspay.airborne.ota.OTADownloadWorker
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager

Expand Down Expand Up @@ -58,8 +59,10 @@ class Airborne(
private val applicationManager = hyperOTAServices.createApplicationManager(airborneInterface.getDimensions())

init {
airborneObjectMap.put(airborneInterface.getNamespace(), this)
airborneObjectMap[airborneInterface.getNamespace()] = this
applicationManager.shouldUpdate = airborneInterface.enableBootDownload()
applicationManager.loadApplication(airborneInterface.getNamespace(), airborneInterface.getLazyDownloadCallback())
OTADownloadWorker.managerMap[airborneInterface.getNamespace()] = applicationManager
}

private fun bootComplete(filePath: String) {
Expand Down Expand Up @@ -93,16 +96,36 @@ class Airborne(
return applicationManager.readReleaseConfig()
}

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


/**
* Check if an OTA update is available.
* Delegates to ApplicationManager which handles the network call and version comparison.
*/
@Keep
fun checkForUpdate(): String {
return applicationManager.checkForUpdate()
}

/**
* 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
* Download and install the latest OTA bundle.
* Delegates to ApplicationManager.downloadUpdate() which reuses the same
* download/install infrastructure as boot-time updates.
*/
@Keep
fun setSslConfig(sslSocketFactory: SSLSocketFactory, trustManager: X509TrustManager) {
applicationManager.setSslConfig(sslSocketFactory, trustManager)
fun downloadUpdate(onComplete: (success: Boolean) -> Unit) {
applicationManager.downloadUpdate(onComplete = onComplete)
}

companion object {
Expand Down Expand Up @@ -145,7 +168,7 @@ class Airborne(
// }
// }

public val airborneObjectMap: MutableMap<String, Airborne> = mutableMapOf()
val airborneObjectMap: MutableMap<String, Airborne> = java.util.concurrent.ConcurrentHashMap()

/**
* Default LazyDownloadCallback implementation.
Expand All @@ -169,5 +192,14 @@ class Airborne(
}
}
}

/**
* Trigger a background OTA download via WorkManager.
* Call from FCM service or any context — does not require RN.
*/
@JvmStatic
fun triggerBackgroundDownload(context: Context, namespace: String) {
OTADownloadWorker.enqueue(context, namespace)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ abstract class AirborneInterface {
open fun getLazyDownloadCallback(): LazyDownloadCallback {
return defaultLazyCallback
}

open fun enableBootDownload(): Boolean = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ class AirborneModule(reactContext: ReactApplicationContext) :
implementation.getBundlePath(namespace, promise)
}

@ReactMethod
fun checkForUpdate(namespace: String, promise: Promise) {
implementation.checkForUpdate(namespace, promise)
}

@ReactMethod
fun downloadUpdate(namespace: String, promise: Promise) {
implementation.downloadUpdate(namespace, promise)
}

@ReactMethod
fun startBackgroundDownload(namespace: String, promise: Promise) {
implementation.startBackgroundDownload(namespace, promise)
}

@ReactMethod
fun reloadApp(namespace: String, promise: Promise) {
implementation.reloadApp(namespace, promise)
}

companion object {
const val NAME = "Airborne"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package `in`.juspay.airborneplugin

import android.os.Handler
import android.os.Looper
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Promise

Expand Down Expand Up @@ -68,4 +70,72 @@ class AirborneModuleImpl(private val reactContext: ReactApplicationContext) {
promise.reject("AIRBORNE_ERROR", "Failed to get bundle path: ${e.message}", e)
}
}

fun checkForUpdate(namespace: String, promise: Promise) {
Thread {
try {
val airborne = Airborne.airborneObjectMap[namespace]
if (airborne == null) {
promise.reject("AIRBORNE_ERROR", "Airborne not initialized for namespace: $namespace")
return@Thread
}
val result = airborne.checkForUpdate()
promise.resolve(result)
} catch (e: Exception) {
promise.reject("AIRBORNE_ERROR", "Failed to check for update: ${e.message}", e)
}
}.start()
}

fun downloadUpdate(namespace: String, promise: Promise) {
try {
val airborne = Airborne.airborneObjectMap[namespace]
if (airborne == null) {
promise.reject("AIRBORNE_ERROR", "Airborne not initialized for namespace: $namespace")
return
}
airborne.downloadUpdate { success ->
Handler(Looper.getMainLooper()).post {
promise.resolve(success)
}
}
} catch (e: Exception) {
promise.reject("AIRBORNE_ERROR", "Failed to download update: ${e.message}", e)
}
}

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


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

} catch (e: Exception) {
promise.reject("AIRBORNE_ERROR", "Failed to reload app: ${e.message}", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ class AirborneTurboModule(reactContext: ReactApplicationContext) :
implementation.getBundlePath(nameSpace, promise)
}

override fun checkForUpdate(nameSpace: String, promise: Promise) {
implementation.checkForUpdate(nameSpace, promise)
}

override fun downloadUpdate(nameSpace: String, promise: Promise) {
implementation.downloadUpdate(nameSpace, promise)
}

override fun startBackgroundDownload(nameSpace: String, promise: Promise) {
implementation.startBackgroundDownload(nameSpace, promise)
}

override fun reloadApp(nameSpace: String, promise: Promise) {
implementation.reloadApp(nameSpace, promise)
}

companion object {
const val NAME = "AirborneReact"
}
Expand Down
4 changes: 4 additions & 0 deletions airborne-react-native/src/NativeAirborne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface Spec extends TurboModule {
readReleaseConfig(nameSpace: string): Promise<string>;
getFileContent(nameSpace: string, filePath: string): Promise<string>;
getBundlePath(nameSpace: string): Promise<string>;
checkForUpdate(nameSpace: string): Promise<string>;
downloadUpdate(nameSpace: string): Promise<boolean>;
startBackgroundDownload(nameSpace: string): Promise<boolean>;
reloadApp(nameSpace: string): Promise<void>;
Comment on lines +22 to +25
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.

}

export default TurboModuleRegistry.getEnforcing<Spec>('Airborne');
16 changes: 16 additions & 0 deletions airborne-react-native/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,20 @@ export function getBundlePath(nameSpace: string): Promise<string> {
return Airborne.getBundlePath(nameSpace);
}

export function checkForUpdate(nameSpace: string): Promise<string> {
return Airborne.checkForUpdate(nameSpace);
}

export function downloadUpdate(nameSpace: string): Promise<boolean> {
return Airborne.downloadUpdate(nameSpace);
}

export function startBackgroundDownload(nameSpace: string): Promise<boolean> {
return Airborne.startBackgroundDownload(nameSpace);
}

export function reloadApp(nameSpace: string): Promise<void> {
return Airborne.reloadApp(nameSpace);
}

export default Airborne;
2 changes: 2 additions & 0 deletions airborne_sdk_android/airborne/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ dependencies {
implementation('com.google.android.material:material:1.12.0')
implementation('androidx.core:core-ktx:1.1.0')
implementation('com.squareup.okhttp3:okhttp:3.3.0')
implementation('androidx.work:work-runtime-ktx:2.9.1')
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3')

api ('in.juspay:hyperutil:2.2.5-xota.05')

Expand Down
Loading
Loading