Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

### Fixes

- **iOS**: `endSigningSession` no longer calls `DSMManager` off the main thread. Expo dispatches a synchronous `AsyncFunction` body on a serial background queue, so `clearAllWebCookies()` and `logout()` were reached off-main on every call, including the one `useDocuSignSigning`'s `reset()` makes between flows. The guard now lives in `clearWebCookiesAsync`, the only method touching `DSMManager` and `WKWebsiteDataStore` directly, so it covers every caller. Thanks to @virajpsimformsolutions for finding and fixing this.
- **iOS**: `reset()` no longer re-enters itself to reach the main thread. The hop sat below the block that cancels an in-flight signing promise, so the re-entrant pass ran that block twice and could cancel a session that claimed the slot in between.
- **iOS**: reject a blank or non-`https` `signingUrl` before presenting. `DSMEnvelopesManager.presentCaptiveSigning` validates nothing and presents unconditionally, so a malformed URL rendered an empty signing controller whose completion never fired and left the promise unsettled. `signingUrl` defaults to `""` when JS omits it, so this was reachable without a malformed URL at all. Brings iOS to parity with the Android guard below.
- **Android**: reject a blank or non-`https` `signingUrl` before launching. The SDK's URL overload validates nothing and calls `startActivity` unconditionally, so a malformed URL opened an empty signing activity and left the promise unsettled.
- **Android**: `presentCaptiveSigning` now clears `currentEnvelopeId` when the launch itself throws, matching the URL path.
Expand Down
32 changes: 21 additions & 11 deletions ios/DocuSignManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,25 @@ internal final class DocuSignManager: NSObject {
/// `loginWithAccessToken` runs. If you need to isolate DocuSign's WebKit
/// state from the rest of your app, use a non-default data store for those
/// other WebViews.
/// Hops to the main thread before touching any DSMManager or WebKit API.
///
/// Expo dispatches a synchronous `AsyncFunction` body on a serial background queue, so every
/// caller that originates in a JS call arrives here off-main. This is the only method reaching
/// `DSMManager.clearAllWebCookies()` and `WKWebsiteDataStore` directly, so one guard here covers
/// `performLogin`, `endSigningSession` and `reset` rather than each hopping for itself. The
/// completion is dispatched on main below, so callers may touch DSMManager from it.
private func clearWebCookiesAsync(completion: @escaping () -> Void) {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
completion()
return
}
self.clearWebCookiesAsync(completion: completion)
}
return
}

DSMManager.clearAllWebCookies()
let dataStore = WKWebsiteDataStore.default()
let types = WKWebsiteDataStore.allWebsiteDataTypes()
Expand Down Expand Up @@ -526,7 +544,6 @@ internal final class DocuSignManager: NSObject {
/// free.
func endSigningSession(completion: @escaping () -> Void) {
// Resolve any in-flight signing promise so the JS side does not hang.
var pendingResolved = false
stateQueue.sync {
if let pending = pendingCompletion {
let outcome = SigningOutcome(
Expand All @@ -538,10 +555,8 @@ internal final class DocuSignManager: NSObject {
pendingCompletion = nil
currentEnvelopeId = nil
DispatchQueue.main.async { pending(.success(outcome)) }
pendingResolved = true
}
}
_ = pendingResolved // silence unused-warning; kept for future telemetry

clearWebCookiesAsync { [weak self] in
guard let self = self else {
Expand Down Expand Up @@ -591,14 +606,9 @@ internal final class DocuSignManager: NSObject {
return
}

if !Thread.isMainThread {
DispatchQueue.main.async { [weak self] in
guard let self = self else { completion(); return }
self.reset(completion: completion)
}
return
}

// No main-thread hop here. clearWebCookiesAsync guards itself, and re-entering reset() from
// main would run the pending-cancellation block above a second time, cancelling any session
// that claimed the slot in between.
clearWebCookiesAsync { [weak self] in
guard let self = self else { completion(); return }
_ = DSMManager.logout()
Expand Down
Loading