From a6775aa5b604ec966207a2d56fe1c9e67cec5823 Mon Sep 17 00:00:00 2001 From: Viraj Date: Tue, 23 Jun 2026 18:37:07 +0530 Subject: [PATCH 1/2] fix(ios): run endSigningSession DSMManager calls on main thread Expo AsyncFunction handlers are dispatched on AsyncFunctionQueue (a non-main queue). DSMManager APIs (clearAllWebCookies, logout) must run on the main thread, so endSigningSession could touch them off-main and intermittently hang or crash. Hop to main before calling clearWebCookiesAsync. Co-Authored-By: Claude Opus 4.8 (1M context) --- ios/DocuSignManager.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ios/DocuSignManager.swift b/ios/DocuSignManager.swift index 7d00dc0..a68ccfb 100644 --- a/ios/DocuSignManager.swift +++ b/ios/DocuSignManager.swift @@ -543,6 +543,16 @@ internal final class DocuSignManager: NSObject { } _ = pendingResolved // silence unused-warning; kept for future telemetry + // DSMManager APIs (clearAllWebCookies, logout) must run on the main thread. + // Expo async functions are dispatched on AsyncFunctionQueue (non-main), so + // we must hop to main before touching any DSMManager API. + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.endSigningSession(completion: completion) + } + return + } + clearWebCookiesAsync { [weak self] in guard let self = self else { completion() From c71e49d14bdf59ac2a16ad537a445e1d85dddd8f Mon Sep 17 00:00:00 2001 From: Viraj Date: Tue, 23 Jun 2026 18:37:36 +0530 Subject: [PATCH 2/2] feat(ios): force-dismiss captive signing modal in reset() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit presentCaptiveSigningWithUrl presents the DocuSign signing WebView as a native modal above the app's view hierarchy. The SDK only dismisses it on the user's own Finish/Cancel — so a host app that tears down the session programmatically (e.g. a remote "end consultation" signal) cannot remove the modal: reset() resolved the JS promise but left the WebView on screen. reset() now dismisses the presented modal on the key window's root view controller. Gated on an in-flight signing (hadPending) so reset() never tears down an unrelated modal when no signing is active. Co-Authored-By: Claude Opus 4.8 (1M context) --- ios/DocuSignManager.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ios/DocuSignManager.swift b/ios/DocuSignManager.swift index a68ccfb..0a678d7 100644 --- a/ios/DocuSignManager.swift +++ b/ios/DocuSignManager.swift @@ -581,8 +581,10 @@ internal final class DocuSignManager: NSObject { /// Safe to call when the SDK was never initialized: returns immediately /// without touching DSMManager. func reset(completion: @escaping () -> Void) { + var hadPending = false stateQueue.sync { if let pending = pendingCompletion { + hadPending = true let outcome = SigningOutcome( status: "cancelled", envelopeId: currentEnvelopeId ?? "", @@ -595,6 +597,23 @@ internal final class DocuSignManager: NSObject { } } + // Force-dismiss the captive signing modal if one is on screen. reset() is + // called on session teardown (e.g. END_CONSULTATION) which can arrive while + // the DocuSign WebView is still presented; the SDK only dismisses its own UI + // on user Finish/Cancel, so we dismiss the presented modal here. Gated on + // hadPending so an unrelated modal is never torn down. + if hadPending { + DispatchQueue.main.async { + let root = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first(where: { $0.isKeyWindow })?.rootViewController + if root?.presentedViewController != nil { + root?.dismiss(animated: true) + } + } + } + let needsTeardown = stateQueue.sync { _isInitialized || _hasLoggedIn } guard needsTeardown else { completion()