Skip to content

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

Merged
pingidentity-gaurav merged 4 commits into
mainfrom
SDKS-5018
Jun 12, 2026
Merged

feat(android): SDKS-5018 introduce launchBridge in rn-core and migrate all scope.launch bridge sites#47
pingidentity-gaurav merged 4 commits into
mainfrom
SDKS-5018

Conversation

@pingidentity-gaurav

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

Copy link
Copy Markdown
Contributor

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

  • Refactor

    • Standardized Android coroutine-to-promise bridging across modules, centralizing promise resolution/rejection, preserving cancellation semantics, and simplifying per-method error handling.
  • Tests

    • Added Robolectric/JUnit tests for the new coroutine bridge covering cancellation, error-code propagation, throwable preservation, context forwarding, and promise settlement behavior.
  • Chore

    • Added Robolectric test dependency to Android build configuration.

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

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@pingidentity-gaurav, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 27 minutes and 38 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25300da6-2430-4672-8bf2-5109b073f61e

📥 Commits

Reviewing files that changed from the base of the PR and between b0ec73e and 878834d.

📒 Files selected for processing (2)
  • packages/core/android/build.gradle
  • packages/oidc/android/src/main/java/com/pingidentity/rnoidc/RNPingOidcCommon.kt
📝 Walkthrough

Walkthrough

Adds a coroutine-to-React-Native Promise bridge (launchBridge), unit/QA tests, and migrates many Android RN bridge methods across modules to use launchBridge with preserved cancellation semantics and consistent promise rejection mapping.

Changes

Android Coroutine-Promise Bridge Refactoring

Layer / File(s) Summary
Bridge utility and build change
packages/core/android/src/main/java/com/pingidentity/rncore/utils/CoroutineBridge.kt, packages/core/android/build.gradle
Adds CoroutineScope.launchBridge(promise, errorCode, context, block) and adds Robolectric testImplementation dependency.
Bridge unit and QA test suites
packages/core/android/src/test/java/com/pingidentity/rncore/utils/CoroutineBridge*.kt
Adds Robolectric/JUnit tests and QA tests with TestPromise and RN Arguments shadows validating cancellation rethrow, error mapping, context forwarding, error-code/throwable passthrough, and settlement semantics.
Journey, Browser, and OIDC migration
packages/journey/.../RNPingJourneyCommon.kt, packages/browser/.../RNPingBrowserCommon.kt, packages/oidc/.../RNPingOidcCommon.kt
Migrates Journey lifecycle, Browser.open, and OIDC client/web/authorize flows from scope.launch { try/catch } to scope.launchBridge(promise, errorCode) { ... }, adding early return@launchBridge cases and Result-based resolve/reject handling.
Binding, FIDO, External IDP, and FIDO tests
packages/binding/.../RNPingBindingCommon.kt, packages/fido/.../RNPingFidoCommon.kt, packages/external-idp/.../RNPingExternalIdpCommon.kt, packages/fido/.../RNPingFidoTest.kt
Migrates binding/signing/key-management and external-IDP flows to launchBridge, refactors FIDO journey methods to use launchBridge, and updates FIDO tests to mock Arguments.createMap/createArray with Java-only implementations.
Device & credential migrations
packages/device-id/.../RNPingDeviceIdCommon.kt, packages/device-profile/.../RNPingDeviceProfileCommon.kt, packages/device-client/.../RNPingDeviceClientCommon.kt, packages/oath/.../RNPingOathCommon.kt
Migrates device-id resolution, device-profile collection (switches to SupervisorJob+Dispatchers.IO), device-client CRUD, and OATH credential flows to launchBridge, adjusts early-exit and per-key deletion error aggregation.
Push module migration
packages/push/.../RNPingPushCommon.kt
Standardizes push operations to use launchBridge, marks coroutine scope @Volatile, and revises cleanup to close clients on a dedicated cleanupScope before recreating the main scope.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tsdamas
  • rodrigoareis
  • george-bafaloukas-forgerock

Poem

🐰 A bridge across coroutines, smooth and bright,
Promises settled by a single light,
Cancellations kept, errors shown with care,
One pattern hops through modules everywhere. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.79% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing launchBridge and migrating 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.

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

✨ Finishing Touches
🧪 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

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-12 15:45 UTC

@coderabbitai coderabbitai Bot left a comment

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.

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

@tsdamas

tsdamas commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Overall, looks good to me. Left minor comments.

@rodrigoareis rodrigoareis left a comment

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.

Overall changes looks good. Left some comments

Comment thread packages/push/android/src/main/java/com/pingidentity/rnpush/RNPingPushCommon.kt Outdated
Comment thread packages/push/android/src/main/java/com/pingidentity/rnpush/RNPingPushCommon.kt Outdated
)
},
onFailure = { error ->
promise.reject(

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.

Should we consider rethrowing and relying on launchBridge with the correct errorCode? Otherwise, we can add a comment explaining the bypass is deliberate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment explaining this is deliberate. The Ping Device Profile SDK wraps all exceptions inside Result.Failure before the block returns, so launchBridge never sees them as thrown exceptions. Mapping errors explicitly in fold.onFailure is the correct pattern here.

* @param id Logger handle identifier from JS.
* @return Native logger instance, or null when missing/invalid.
*/
private fun resolveLoggerFromCore(id: String?): Logger? {

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 was a pre-existing issue but this PR. But now I noticed other packages have an identical private copy of this method. I suggest extract it to a top-level utility in rn-core/utils/ (e.g., LoggerResolver.kt) to keep a single source of truth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pre-existing across all packages. com.pingidentity.logger is not a dependency of rn-core and we want to keep it that way since rn-core currently has no native SDK dependencies beyond react-android. Extracting the helper there would break that architectural boundary. The per-package copies are a one-liner each wrapping the logger registry resolve. Happy to revisit if there is appetite for a separate shared utility module outside of rn-core.

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 justification is architecturally valid, but accept duplication bugs me. I think there are better paths, as you suggested above. I am ok to keep it for now. We will probably need in a near future a new rn-android-utils / rn-bridge-core module that houses resolveNativeLogger, and potentially other helpers (DeviceType constants, etc.), then we can address it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I m gonna add that in as an exploration task.

- Fix KDoc on launchBridge: promise param only handles rejection, not
  success settlement (caller's responsibility inside the block)
- Remove unused android.util.Log import in rn-device-profile
- Add explanatory comment on fold.onFailure bypass in rn-device-profile
  (SDK wraps exceptions in Result.Failure before launchBridge can see them)
- Add import and de-qualify Arguments calls in rn-binding for consistency
- Sort imports in rn-push alphabetically
- Add @volatile to scope field in rn-push for cross-thread visibility
- Fix push cleanup() to close PushClient instances before clearing the
  registry, using a dedicated short-lived scope since close() is suspend
- Restructure device-client get/update/deleteDevice to move result.fold
  outside the try/catch, preventing potential double-rejection if
  onFailure's rejectThrowable were to throw
- Add CancellationException rethrow comments in oath, device-client,
  external-idp, push explaining why the inner rethrow is necessary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@rodrigoareis rodrigoareis left a comment

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.

Changes looks good to me

@tsdamas tsdamas left a comment

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.

LGTM

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cross-platform consistency review of the launchBridge migration. The migration is clean; two sites conflate Kotlin coroutine CancellationException (structured-concurrency teardown) with user-initiated browser cancel, which contradicts both launchBridge's documented contract and the iOS RN bridge.

Verified against iOS (packages/browser/ios, packages/oidc/ios) and both native SDKs: iOS resolves {type:"cancel"} only for the dedicated user-cancel signals (BrowserError.externalUserAgentCancelled, ASWebAuthenticationSessionError.canceledLogin) and has no catch is CancellationError — Swift Task cancellation is rejected, never resolved as cancel. On both platforms the native user-cancel signal is a distinct type from the cooperative-cancellation primitive. See the 2 inline comments.

payload.putString("type", "cancel")
promise.resolve(payload)
return@launch
return@launchBridge

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CancellationException is swallowed and resolved as a {type:"cancel"} payload here (the if (error is BrowserCanceledException || error is CancellationException) at line 230), defeating the migration's purpose.

The inner catch (e: Exception) { Result.failure(e) } (~line 211) captures coroutine CancellationException into the Result, and this branch then resolves {type:"cancel"} for it. launchBridge exists precisely to re-throw CancellationException so scope cancellation propagates without settling the promise — this site subverts that.

Inconsistent with iOS: packages/browser/ios/RNPingBrowserCommon.swift resolves {type:"cancel"} only for BrowserError.externalUserAgentCancelled / ASWebAuthenticationSessionError.canceledLogin, and lets Swift CancellationError reject through the generic catch. BrowserCanceledException (Android's genuine user-cancel) is a plain RuntimeException, unrelated to kotlinx.coroutines.CancellationException, and is returned via normal suspend completion — not via coroutine cancellation.

Suggested fix: drop the || error is CancellationException disjunct at line 230, and stop catching CancellationException in the inner catch (e: Exception) (re-throw it). Keep BrowserCanceledException -> {type:"cancel"}. Genuine user cancels are unaffected; structured-concurrency cancellation then propagates per the launchBridge contract and matches iOS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Added catch (e: CancellationException) { throw e } before catch (e: Exception) so CancellationException is never boxed into Result.failure, and dropped the || error is CancellationException disjunct from the result.exceptionOrNull() check. The kotlinx.coroutines.CancellationException import is still needed for the rethrow clause. BrowserCanceledException to {type:"cancel"} is unchanged.

canceled.putString("type", "cancel")
promise.resolve(canceled)
return@launch
return@launchBridge

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same CancellationException-as-cancel conflation as the browser module — at both the inner catch (e: Exception) check (line 390) and this result.exceptionOrNull() check (line 408).

The catch (e: Exception) pre-empts launchBridge's CancellationException re-throw, and the || ... is CancellationException branches resolve {type:"cancel"} for structured-concurrency cancellation. iOS (packages/oidc/ios/RNPingOidcCommon.swift) resolves cancel only for BrowserError.externalUserAgentCancelled / ASWebAuthenticationSessionError.canceledLogin and rejects Swift CancellationError via the generic catch — it does not conflate the two.

Suggested fix: remove the || ... is CancellationException disjunct at both line 390 and line 408 (keep BrowserCanceledException), and let CancellationException propagate to launchBridge. This restores the documented contract and matches iOS; the real user-cancel path is carried by BrowserCanceledException via normal completion, so it is not regressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Same pattern as the browser module. Added a dedicated catch (e: CancellationException) { throw e } guard before the catch (e: Exception) block, removed || e is CancellationException from the inner if check, and dropped || error is CancellationException from the result.exceptionOrNull() check. Import retained for the rethrow clause.

…nch flows

CancellationException was being caught by the inner catch (e: Exception)
block and resolved as {type:"cancel"}, settling the JS promise instead of
letting launchBridge propagate structured-concurrency cancellation. Added
a dedicated catch (e: CancellationException) { throw e } guard before each
inner Exception handler and removed the spurious || error is
CancellationException disjuncts from the result.exceptionOrNull() checks.
BrowserCanceledException (the real user-cancel signal) is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@pingidentity-gaurav pingidentity-gaurav merged commit 16f6c91 into main Jun 12, 2026
12 of 13 checks passed
@pingidentity-gaurav pingidentity-gaurav deleted the SDKS-5018 branch June 12, 2026 15:45
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.

4 participants