Why this matters now
With useProofStatus (Issue 4), automatic re-authentication on 401 (via refreshAuthToken), and Lobstr wallet connect all potentially triggering openLobstrForSigning from different code paths, the chance of concurrent calls is real. A second openLobstrForSigning call silently clobbers _pendingResolve / _pendingReject, causing the first caller to never settle and the second to receive the first caller's signed XDR if Lobstr routes the callback to the wrong context.
Problem / What
src/services/lobstr.ts uses module-level mutable singletons:
let _pendingResolve: CallbackResolve | null = null;
let _pendingReject: CallbackReject | null = null;
cancelLobstrCallback is called at the top of openLobstrForSigning, which means a second signing request silently cancels the first. The cancel rejection is swallowed by the first caller with no signal.
The correct design is a keyed pending-call registry (a Map<string, {resolve, reject, timer}>) where each call gets a unique correlation ID, and resolveLobstrCallback routes the callback to the correct entry by matching a correlation_id query param that was embedded in the SEP-7 URI's callback URL.
Key Challenges
- The SEP-7
callback URL must embed a correlation ID that Lobstr echoes back in the deep-link. The ecotask://lobstr/callback?xdr=<XDR>&id=<correlationId> format is the minimal change to buildSep7TxUri and parseLobstrCallbackUrl.
- The
resolveLobstrCallback function in RootNavigator parses the URL — it must extract the correlation ID and route to the correct pending entry.
- All existing callers pass the Lobstr callback URL opaquely and don't need to know about correlation IDs.
- The timeout from Issue 3 should be stored per-entry in the registry.
lobstr.test.ts must test the concurrent-call scenario.
Acceptance Criteria
Relevant files / functions
src/services/lobstr.ts — openLobstrForSigning, resolveLobstrCallback, cancelLobstrCallback, buildSep7TxUri, parseLobstrCallbackUrl
src/navigation/RootNavigator.tsx — handleUrl
src/__tests__/lobstr.test.ts
Out of scope
- Changing the Lobstr deep-link scheme registration (AndroidManifest / Info.plist).
- UI for managing multiple pending signing requests.
Why this matters now
With
useProofStatus(Issue 4), automatic re-authentication on 401 (viarefreshAuthToken), and Lobstr wallet connect all potentially triggeringopenLobstrForSigningfrom different code paths, the chance of concurrent calls is real. A secondopenLobstrForSigningcall silently clobbers_pendingResolve/_pendingReject, causing the first caller to never settle and the second to receive the first caller's signed XDR if Lobstr routes the callback to the wrong context.Problem / What
src/services/lobstr.tsuses module-level mutable singletons:cancelLobstrCallbackis called at the top ofopenLobstrForSigning, which means a second signing request silently cancels the first. The cancel rejection is swallowed by the first caller with no signal.The correct design is a keyed pending-call registry (a
Map<string, {resolve, reject, timer}>) where each call gets a unique correlation ID, andresolveLobstrCallbackroutes the callback to the correct entry by matching acorrelation_idquery param that was embedded in the SEP-7 URI's callback URL.Key Challenges
callbackURL must embed a correlation ID that Lobstr echoes back in the deep-link. Theecotask://lobstr/callback?xdr=<XDR>&id=<correlationId>format is the minimal change tobuildSep7TxUriandparseLobstrCallbackUrl.resolveLobstrCallbackfunction inRootNavigatorparses the URL — it must extract the correlation ID and route to the correct pending entry.lobstr.test.tsmust test the concurrent-call scenario.Acceptance Criteria
openLobstrForSigningcalls each receive their own{resolve, reject, timer}entry keyed by a unique correlation ID.resolveLobstrCallbackroutes to the correct entry using theidquery param.cancelLobstrCallbackwithout an ID cancels all pending entries (for clean teardown on logout/disconnect).Relevant files / functions
src/services/lobstr.ts—openLobstrForSigning,resolveLobstrCallback,cancelLobstrCallback,buildSep7TxUri,parseLobstrCallbackUrlsrc/navigation/RootNavigator.tsx—handleUrlsrc/__tests__/lobstr.test.tsOut of scope