Skip to content

feat(android): SDKS-5018 introduce launchBridge in rn-core and migrate all scope.launch bridge sites#47

Open
pingidentity-gaurav wants to merge 1 commit into
mainfrom
SDKS-5018
Open

feat(android): SDKS-5018 introduce launchBridge in rn-core and migrate all scope.launch bridge sites#47
pingidentity-gaurav wants to merge 1 commit into
mainfrom
SDKS-5018

Conversation

@pingidentity-gaurav
Copy link
Copy Markdown
Contributor

@pingidentity-gaurav pingidentity-gaurav commented Jun 1, 2026

Summary

  • Adds launchBridge Kotlin extension on CoroutineScope in rn-core/utils/CoroutineBridge.kt — re-throws CancellationException for correct structured-concurrency propagation, maps all other Throwables to GenericError via mapThrowableToGenericError, and settles the React Native promise
  • Migrates all 48 promise-settling scope.launch bridge call sites across 11 Android packages to use launchBridge
  • Fixes rn-device-profile missing SupervisorJob() on its CoroutineScope

Packages migrated: rn-binding, rn-browser, rn-device-client, rn-device-id, rn-device-profile, rn-external-idp, rn-fido, rn-journey, rn-oath, rn-oidc, rn-push

Not migrated (intentional): rn-logger sync() (fire-and-forget, no promise) and rn-push event-emission sites (no promise)

Problem

Every scope.launch bridge site caught Throwable or Exception uniformly, which swallowed CancellationException. This violated Kotlin structured concurrency: scope cancellation produced spurious JS promise rejections instead of propagating cleanly.

Approach

Three migration patterns depending on the package's error-handling needs:

  • Category A — inner try/catch removed entirely, launchBridge outer catch handles all errors via mapThrowableToGenericError (rn-device-id, rn-oidc simple sites)
  • Category B — inner try/catch kept with catch (e: CancellationException) { throw e } added before existing catch, preserving package-local error mappers (rn-oath, rn-device-client, rn-external-idp, rn-push)
  • Category C — inner try/catch completely unchanged, only outer scope.launch wrapped (rn-browser, rn-oidc authorize site)

Test plan

  • 390 Android unit tests pass across all packages (./gradlew testDebugUnitTest — BUILD SUCCESSFUL)
  • All existing error-code assertions pass unchanged
  • CoroutineBridgeTest.kt — 8 unit tests covering re-throw, error type mapping, context forwarding
  • CoroutineBridgeQaTest.kt — 5 integration tests covering live scope cancellation, throwable identity, context passthrough
  • Sample app builds and launches on emulator and smoke tested

Summary by CodeRabbit

Release Notes

  • Refactor

    • Refactored Android coroutine-to-promise bridging across all modules (binding, authentication, device management, external identity providers, FIDO, OATH, OIDC, push) to standardize error handling and improve cancellation semantics.
  • Tests

    • Added Robolectric-based unit tests for core coroutine bridge utilities, verifying error code preservation, exception handling, proper promise settlement, and cancellation behavior.

Introduces launchBridge coroutine extension in rn-core that re-throws
CancellationException for correct structured-concurrency propagation,
and settles the React Native promise via GenericError for all other
throwables. Migrates all 48 scope.launch bridge call sites across 11
Android packages to use it.

Packages migrated: rn-binding, rn-device-profile, rn-fido, rn-journey,
rn-device-id, rn-oidc, rn-oath, rn-device-client, rn-external-idp,
rn-browser, rn-push. Packages with no promise-settling launch sites
(rn-logger) and non-promise event-emission sites (rn-push flush/token)
are intentionally left as scope.launch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR centralizes React Native promise-based coroutine execution across 11 Android SDK modules by introducing a new launchBridge utility function. The bridge captures the recurrent pattern of launching a coroutine, handling cancellation semantics, and settling a JavaScript promise. Each module's async entry points are migrated from manual scope.launch { try/catch ... } to scope.launchBridge(promise, errorCode) { ... }, removing boilerplate exception mapping and promise rejection code.

Changes

Android Coroutine-Promise Bridge Refactoring

Layer / File(s) Summary
Bridge utility and test infrastructure
packages/core/android/src/main/java/com/pingidentity/rncore/utils/CoroutineBridge.kt, packages/core/android/build.gradle
Adds CoroutineScope.launchBridge(promise, errorCode, context, block) extension that launches a suspend block, rethrows CancellationException to preserve structured cancellation, and rejects the promise with a mapped GenericError for other throwables. Robolectric 4.11.1 test dependency added.
Bridge unit and QA test suites
packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridge*.kt
Comprehensive JUnit/Robolectric test files with custom TestPromise implementations and Robolectric shadows verify launchBridge correctly rethrows cancellation, maps error types, preserves error codes and throwable identity, forwards coroutine context, and does not auto-reject on success.
Journey, Browser, and OIDC lifecycle migration
packages/journey/android/src/main/java/com/pingidentity/rnjourney/RNPingJourneyCommon.kt, packages/browser/android/src/main/java/com/pingidentity/rnbrowser/RNPingBrowserCommon.kt, packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt
Journey start, next, resume, getSession, refresh, revoke, userinfo, ssoToken, logout and Browser open and OIDC authorize, client token/user methods migrate to launchBridge, removing local try/catch blocks. Explicit early rejection added for missing user/flow state.
Credential and device operation migration
packages/oath/android/src/main/java/com/pingidentity/rnoath/RNPingOathCommon.kt, packages/device-client/android/src/main/java/com/pingidentity/rndeviceclient/RNPingDeviceClientCommon.kt, packages/device-id/android/src/main/java/com/pingidentity/rndeviceid/RNPingDeviceIdCommon.kt, packages/device-profile/android/src/main/java/com/pingidentity/rndeviceprofile/RNPingDeviceProfileCommon.kt
OATH credential operations, device-client get/update/deleteDevice, device-ID resolution, and device-profile collection migrate to launchBridge with explicit CancellationException rethrow. Device-profile scope updated to use SupervisorJob.
Binding, FIDO, External IDP, and Push migration
packages/binding/android/src/main/java/com/pingidentity/rnbinding/RNPingBindingCommon.kt, packages/fido/android/src/main/java/com/pingidentity/rnfido/RNPingFidoCommon.kt, packages/external-idp/android/src/main/java/com/pingidentity/rnexternalidp/RNPingExternalIdpCommon.kt, packages/push/android/src/main/java/com/pingidentity/rnpush/RNPingPushCommon.kt, packages/fido/android/src/test/java/com/pingidentity/rnfido/RNPingFidoTest.kt
Binding/signing/key-management, FIDO operations, external-IDP authorize/setSelectedProvider, and Push credential/notification/token operations migrate to launchBridge. FIDO tests updated to mock React Native Arguments factory methods with Java-only implementations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • tsdamas

Poem

🐰 A bridge across coroutines, strong and fair,
Promise settlement wrapped with utmost care—
No more try-catch scattered far and wide,
One pattern guides the async tide! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: introducing launchBridge and migrating all scope.launch bridge sites across Android packages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDKS-5018

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.

@pingidentity-gaurav pingidentity-gaurav changed the title feat(android): introduce launchBridge in rn-core and migrate all scope.launch bridge sites feat(android): SDKS-5018 introduce launchBridge in rn-core and migrate all scope.launch bridge sites Jun 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ForgeRock.github.io/ping-react-native-sdk/docs-preview/pr-47/

Built to branch gh-pages at 2026-06-01 23:17 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copy link
Copy Markdown
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/browser/android/src/main/java/com/pingidentity/rnbrowser/RNPingBrowserCommon.kt (1)

206-235: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid resolving the promise on CancellationException; rethrow instead.

In RNPingBrowserCommon.kt, the inner catch (e: Exception) turns CancellationException into Result.failure, and the later branch treats it as a "cancel" outcome. This settles the JS promise instead of allowing coroutine cancellation to propagate.

🛠️ Proposed adjustment
       val result = try {
         val resolvedRedirectUri = redirectUri?.toUri() ?: browserLauncher.redirectUri
         browserLauncher.launch(launchUrl, resolvedRedirectUri)
+      } catch (e: CancellationException) {
+        throw e
       } catch (e: Exception) {
         Result.failure(e)
       }

Drop || error is CancellationException from the "cancel" handling so only BrowserCanceledException maps to { type: "cancel" }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/browser/android/src/main/java/com/pingidentity/rnbrowser/RNPingBrowserCommon.kt`
around lines 206 - 235, The current try/catch in the launch flow converts all
Exceptions (including CancellationException) into Result.failure, which later
leads to settling the JS promise as a "cancel" rather than propagating coroutine
cancellation; update the catch in RNPingBrowserCommon.kt (the block that
computes `val result = try { ... } catch (e: Exception) { ... }`) to rethrow
CancellationException (e.g., if (e is CancellationException) throw e) and
otherwise return Result.failure(e), and then remove `|| error is
CancellationException` from the later cancel-handling branch so only
BrowserCanceledException maps to `{ type: "cancel" }`.
🧹 Nitpick comments (2)
packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridgeTest.kt (1)

70-156: ⚡ Quick win

Extract TestPromise (and the Arguments shadow) into a shared test fixture. This same ~90-line Promise stub and the @Implements shadow are duplicated verbatim in CoroutineBridgeQaTest.kt. Both live in the same package/source set, so a single shared TestPromise/shadow would remove the duplication and prevent the two copies from drifting as the Promise interface evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridgeTest.kt`
around lines 70 - 156, Duplicate TestPromise and the Arguments `@Implements`
shadow exist in CoroutineBridgeTest and CoroutineBridgeQaTest; extract them into
a single shared test fixture (e.g., a new file in the same test package)
containing the TestPromise class and the Arguments shadow implementation, keep
the same package and annotation details so Robolectric/shadowing still works,
remove the duplicate definitions from CoroutineBridgeTest and
CoroutineBridgeQaTest, and update their imports/usages to reference the shared
TestPromise and shared Argument shadow to avoid drift as the Promise interface
evolves.
packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt (1)

383-398: ⚡ Quick win

OIDC authorize intentionally maps CancellationException to a "cancel" payload

  • This mirrors the Android browser bridge (RNPingBrowserCommon.kt), which also converts CancellationException to { type: "cancel" } instead of letting launchBridge rethrow.
  • launchBridge only rethrows CancellationException when it escapes the block; if you want cleanup/scope cancellation to propagate without settling the JS promise (per launchBridge docs), then don’t catch CancellationException here—otherwise this should be documented as “cleanup cancellation surfaces as cancel”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt`
around lines 383 - 398, The current catch in RNPingOidcCommon.kt around the
authorize call swallows CancellationException (and maps it to a "cancel" JS
payload), preventing scope cancellation from propagating via launchBridge;
update the error handling in the authorize block (the try/catch that wraps
handle.web.authorize in launchBridge) so CancellationException is not caught and
instead is rethrown (or remove CancellationException from the caught types)
while still mapping BrowserCanceledException to the `{ type: "cancel" }`
payload; ensure CancellationException is allowed to escape so launchBridge can
handle scope cancellation as intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/core/android/src/main/java/com/pingidentity/rncore/utils/CoroutineBridge.kt`:
- Line 28: The KDoc for launchBridge incorrectly states that the provided
promise is settled on success or failure; in reality launchBridge only rejects
the promise on non-cancellation failures and does not call resolve for
successful completions. Update the KDoc for the function launchBridge and its
param promise to clarify that callers are responsible for resolving the promise
on success and that launchBridge will only reject the promise on
non-cancellation errors (or leave resolution to the provided block). Refer to
the launchBridge function and the promise parameter in the comment to ensure the
wording matches the implemented behavior.

---

Outside diff comments:
In
`@packages/browser/android/src/main/java/com/pingidentity/rnbrowser/RNPingBrowserCommon.kt`:
- Around line 206-235: The current try/catch in the launch flow converts all
Exceptions (including CancellationException) into Result.failure, which later
leads to settling the JS promise as a "cancel" rather than propagating coroutine
cancellation; update the catch in RNPingBrowserCommon.kt (the block that
computes `val result = try { ... } catch (e: Exception) { ... }`) to rethrow
CancellationException (e.g., if (e is CancellationException) throw e) and
otherwise return Result.failure(e), and then remove `|| error is
CancellationException` from the later cancel-handling branch so only
BrowserCanceledException maps to `{ type: "cancel" }`.

---

Nitpick comments:
In
`@packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridgeTest.kt`:
- Around line 70-156: Duplicate TestPromise and the Arguments `@Implements` shadow
exist in CoroutineBridgeTest and CoroutineBridgeQaTest; extract them into a
single shared test fixture (e.g., a new file in the same test package)
containing the TestPromise class and the Arguments shadow implementation, keep
the same package and annotation details so Robolectric/shadowing still works,
remove the duplicate definitions from CoroutineBridgeTest and
CoroutineBridgeQaTest, and update their imports/usages to reference the shared
TestPromise and shared Argument shadow to avoid drift as the Promise interface
evolves.

In
`@packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt`:
- Around line 383-398: The current catch in RNPingOidcCommon.kt around the
authorize call swallows CancellationException (and maps it to a "cancel" JS
payload), preventing scope cancellation from propagating via launchBridge;
update the error handling in the authorize block (the try/catch that wraps
handle.web.authorize in launchBridge) so CancellationException is not caught and
instead is rethrown (or remove CancellationException from the caught types)
while still mapping BrowserCanceledException to the `{ type: "cancel" }`
payload; ensure CancellationException is allowed to escape so launchBridge can
handle scope cancellation as intended.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0033052-1b2c-4265-ba91-2a9f7139ff77

📥 Commits

Reviewing files that changed from the base of the PR and between e2b610d and a1efce3.

📒 Files selected for processing (16)
  • packages/binding/android/src/main/java/com/pingidentity/rnbinding/RNPingBindingCommon.kt
  • packages/browser/android/src/main/java/com/pingidentity/rnbrowser/RNPingBrowserCommon.kt
  • packages/core/android/build.gradle
  • packages/core/android/src/main/java/com/pingidentity/rncore/utils/CoroutineBridge.kt
  • packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridgeQaTest.kt
  • packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridgeTest.kt
  • packages/device-client/android/src/main/java/com/pingidentity/rndeviceclient/RNPingDeviceClientCommon.kt
  • packages/device-id/android/src/main/java/com/pingidentity/rndeviceid/RNPingDeviceIdCommon.kt
  • packages/device-profile/android/src/main/java/com/pingidentity/rndeviceprofile/RNPingDeviceProfileCommon.kt
  • packages/external-idp/android/src/main/java/com/pingidentity/rnexternalidp/RNPingExternalIdpCommon.kt
  • packages/fido/android/src/main/java/com/pingidentity/rnfido/RNPingFidoCommon.kt
  • packages/fido/android/src/test/java/com/pingidentity/rnfido/RNPingFidoTest.kt
  • packages/journey/android/src/main/java/com/pingidentity/rnjourney/RNPingJourneyCommon.kt
  • packages/oath/android/src/main/java/com/pingidentity/rnoath/RNPingOathCommon.kt
  • packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt
  • packages/push/android/src/main/java/com/pingidentity/rnpush/RNPingPushCommon.kt

* mapped to a [com.pingidentity.rncore.error.GenericError] via
* [mapThrowableToGenericError] and the promise is rejected with the resulting error.
*
* @param promise The React Native promise to settle on success or failure.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

KDoc overstates what launchBridge settles. The function only rejects the promise on non-cancellation failure; success settlement (resolve) is the block's responsibility (as the tests confirm). The @param promise text "to settle on success or failure" can mislead callers into omitting their own resolve.

📝 Suggested wording
- * `@param` promise The React Native promise to settle on success or failure.
+ * `@param` promise The React Native promise rejected on non-cancellation failure.
+ *   On success the promise is left untouched; the [block] is responsible for resolving it.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param promise The React Native promise to settle on success or failure.
* `@param` promise The React Native promise rejected on non-cancellation failure.
* On success the promise is left untouched; the [block] is responsible for resolving it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/core/android/src/main/java/com/pingidentity/rncore/utils/CoroutineBridge.kt`
at line 28, The KDoc for launchBridge incorrectly states that the provided
promise is settled on success or failure; in reality launchBridge only rejects
the promise on non-cancellation failures and does not call resolve for
successful completions. Update the KDoc for the function launchBridge and its
param promise to clarify that callers are responsible for resolving the promise
on success and that launchBridge will only reject the promise on
non-cancellation errors (or leave resolution to the provided block). Refer to
the launchBridge function and the promise parameter in the comment to ensure the
wording matches the implemented behavior.

},
onFailure = { err -> DeviceErrorClassifier.rejectThrowable(promise, err) },
)
} catch (e: CancellationException) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we re-throwing the CancellationException when the launchBridge already does that?

)
}
)
} catch (e: CancellationException) {
Copy link
Copy Markdown
Contributor

@tsdamas tsdamas Jun 3, 2026

Choose a reason for hiding this comment

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

Same here and on line 299

private const val LOCATION_SERVICES_CLASS =
"com.google.android.gms.location.LocationServices"
private val scope = CoroutineScope(Dispatchers.IO)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a good improvement.

val credential = deserializeCredential(credentialMap)
val saved = client.saveCredential(credential).getOrThrow()
promise.resolve(serializeCredential(saved))
} catch (e: CancellationException) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here and in oath

@tsdamas
Copy link
Copy Markdown
Contributor

tsdamas commented Jun 3, 2026

Overall, looks good to me. Left minor comments.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants