Skip to content

fix(ios): run endSigningSession DSMManager calls on the main thread - #8

Merged
IronTony merged 3 commits into
mainfrom
fix/ios-endsigningsession-main-thread
Aug 29, 2026
Merged

fix(ios): run endSigningSession DSMManager calls on the main thread#8
IronTony merged 3 commits into
mainfrom
fix/ios-endsigningsession-main-thread

Conversation

@IronTony

Copy link
Copy Markdown
Owner

Takes the main-thread fix from #2 so it can ship in 2.0.0 without waiting on the modal-dismissal work in that PR, which is a separate design question. First commit is @virajpsimformsolutions' original, cherry-picked unchanged. The two after it are the review follow-up.

Merge with rebase, not squash. Squashing collapses the contributor's commit into mine and drops the attribution.

The bug

Expo dispatches a synchronous AsyncFunction body on a serial background queue, so JS-originated calls arrive off-main. DSMManager and WKWebsiteDataStore are main-thread only. endSigningSession reached clearAllWebCookies() and logout() directly from that background queue on every call, including the one useDocuSignSigning's reset() makes between signing flows.

performLogin already hopped to main, which is why this only bit the teardown paths.

The follow-up

The hop landed inside endSigningSession, below the stateQueue.sync that cancels an in-flight signing promise. Re-entering the function from main therefore ran that cancellation block a second time. Empty slot makes it harmless, but a presentCaptiveSigning arriving between the two passes gets cancelled by the redundant one.

Moved the guard into clearWebCookiesAsync. It is the only method reaching DSMManager.clearAllWebCookies() and WKWebsiteDataStore directly, so one guard covers performLogin, endSigningSession and reset, and no caller has to know the threading contract. Its completion is already wrapped in DispatchQueue.main.async, so the DSMManager.logout() each caller runs from it stays on main.

reset() loses its own hop for the same reason. It sat below the same cancellation block and had the same re-entrant double-execution, so this closes that race as well.

Also drops a pendingResolved var in endSigningSession that was assigned, never read, and carried a line existing only to silence the resulting warning.

Verification

swiftc -parse clean, npm run build, npm run lint, 18/18 Jest.

A threading review traced every stateQueue call site for a cycle with main and found none, confirmed logout() and removeObserver still land on main through the completion dispatch, and confirmed completion() is invoked exactly once on every path including reset()'s needsTeardown early return.

Stating the limit plainly, same as #7: CI compiles no Swift and runs no iOS tests, so nothing here catches a type error in this file. Verified by parsing and by reading. A device run before tagging 2.0.0 is the honest bar.

Not included from #2

The force-dismiss commit stays there. It dismisses from the key window's root view controller while both present paths use topmostViewController(), so it tears down the host app's own modal whenever signing was started from a sheet. Fixing that needs a reference to the controller actually presented, which touches the present path too. Tracked on #2.

virajpsimformsolutions and others added 3 commits August 29, 2026 14:41
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) <noreply@anthropic.com>
The hop landed in endSigningSession, below the stateQueue.sync that cancels
an in-flight signing promise, so the re-entrant pass ran that block a second
time. Harmless when the slot is already empty, but a presentCaptiveSigning
arriving in between would be cancelled by the re-entry.

clearWebCookiesAsync is the only method reaching DSMManager.clearAllWebCookies()
and WKWebsiteDataStore directly, so guarding it covers performLogin,
endSigningSession and reset from one place, and no caller has to know the
threading contract. Its completion is already dispatched on main, so the
DSMManager.logout() every caller runs from it stays safe.

reset() loses its own hop for the same reason: it sat below the same
cancellation block and had the same re-entrant double-execution.

Drop the pendingResolved flag in endSigningSession while here. It was
assigned, never read, and carried a line existing only to silence the
resulting warning.
@IronTony IronTony self-assigned this Aug 29, 2026
@IronTony IronTony added the bug Something isn't working label Aug 29, 2026
@IronTony
IronTony merged commit dc34778 into main Aug 29, 2026
6 checks passed
@IronTony IronTony mentioned this pull request Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants