Skip to content

fix(rn-window, wallets): auto-recover on sendAction timeout and retry TEE requests#1610

Closed
jcurbelo wants to merge 3 commits intomainfrom
fix/stellar-approve-timeout-recovery
Closed

fix(rn-window, wallets): auto-recover on sendAction timeout and retry TEE requests#1610
jcurbelo wants to merge 3 commits intomainfrom
fix/stellar-approve-timeout-recovery

Conversation

@jcurbelo
Copy link
Copy Markdown
Contributor

@jcurbelo jcurbelo commented Mar 3, 2026

Summary

Fixes the Stellar wallet.approve 30s timeout on React Native (Pylon #5822 — Marshall Islands + SpotPay). The open-signer frame fails to initialize/respond on low-end devices, and the SDK had no recovery mechanism on RN.

Three complementary fixes:

  • WebViewParent.sendAction auto-recovery (rn-window/Parent.ts): Checks isConnected before sending — if disconnected, awaits the existing handshake (single-flight) or reloads the WebView. On timeout, reloads and retries once. Addresses PR feat(rn-window): auto-recover on sendAction when WebView handshake failed #1599 feedback from Greptile (race condition) and Devin (recovery gating).
  • TEE request retry (ncs-signer.ts): Adds intervalMs: 3_000 to DEFAULT_EVENT_OPTIONS so get-status, sign, etc. retry every 3s within the 30s window instead of sending once.
  • initializeWebView handshake wait (CrossmintWalletProvider.tsx): Now polls for isConnected === true (not just ref existence) with a 30s max wait to match the handshake timeout.

Supersedes #1599 — includes its isConnected pre-check plus timeout recovery and the other two fixes.

Test plan

  • @crossmint/client-sdk-rn-window tests pass (9/9)
  • @crossmint/wallets-sdk tests pass (194/194)
  • Manual test on Expo demo app with injected IndexedDB delay to simulate slow frame init
  • Verify on low-end Android device (Marshall Islands repro scenario)

🤖 Generated with Claude Code


Open with Devin

… TEE requests

WebViewParent.sendAction now verifies handshake before sending and
retries once after reloading the WebView on timeout. TEE event options
add intervalMs (3s) for periodic retries. initializeWebView waits for
handshake completion instead of just ref existence.

Fixes Stellar wallet.approve 30s timeout on React Native (Pylon #5822).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 3, 2026

🦋 Changeset detected

Latest commit: afe21ec

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@crossmint/client-sdk-rn-window Patch
@crossmint/wallets-sdk Patch
@crossmint/client-sdk-react-native-ui Patch
expo-demo Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/wallets-quickstart-devkit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +107 to +131
if (!this.isConnected) {
await this.ensureConnected();
}

if (this.isRecoverableError(response)) {
console.info(`[WebViewParent] Recoverable error (code: ${response.code}), reloading and retrying`);
await this.reloadAndHandshake();
return await super.sendAction(args);
try {
const response = await super.sendAction(args);

if (this.isRecoverableError(response)) {
console.info(
`[WebViewParent] Recoverable error (code: ${(response as any).code}), reloading and retrying`
);
await this.reloadAndHandshake();
return await super.sendAction(args);
}

return response;
} catch (error) {
if (typeof error === "string" && error.includes("Timed out")) {
console.info("[WebViewParent] sendAction timed out, reloading WebView and retrying");
await this.reloadAndHandshake();
return await super.sendAction(args);
}
throw error;
}
}
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.

Chained recovery can triple the reload wait time

When the WebView is fully unresponsive, the error handling paths can chain up to 3 reload+handshake cycles:

  1. ensureConnected()handshakeWithChild() times out (~30s)
  2. ensureConnected() catches → reloadAndHandshake() → handshake times out again (~30s), throws
  3. The thrown timeout string error propagates to the catch block at line 124, which matches "Timed out" → triggers another reloadAndHandshake() + super.sendAction() (~30s+)

This means worst-case latency before final failure is ~90s+, which is significant for a fix targeting a 30s timeout on low-end devices. Consider either:

  • Tracking whether ensureConnected already attempted a reload, so the timeout catch doesn't reload a third time
  • Re-throwing the error from ensureConnected without entering the timeout catch path (e.g. wrapping it in an Error object to distinguish it from sendAction timeouts)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/client/rn-window/src/rn-webview/Parent.ts
Line: 107-131

Comment:
**Chained recovery can triple the reload wait time**

When the WebView is fully unresponsive, the error handling paths can chain up to 3 reload+handshake cycles:

1. `ensureConnected()``handshakeWithChild()` times out (~30s)
2. `ensureConnected()` catches → `reloadAndHandshake()` → handshake times out again (~30s), throws
3. The thrown timeout string error propagates to the `catch` block at line 124, which matches `"Timed out"` → triggers _another_ `reloadAndHandshake()` + `super.sendAction()` (~30s+)

This means worst-case latency before final failure is ~90s+, which is significant for a fix targeting a 30s timeout on low-end devices. Consider either:
- Tracking whether `ensureConnected` already attempted a reload, so the timeout catch doesn't reload a third time
- Re-throwing the error from `ensureConnected` without entering the timeout catch path (e.g. wrapping it in an `Error` object to distinguish it from `sendAction` timeouts)

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 3, 2026

🔥 Smoke Test Results

Status: Passed

Statistics

  • Total Tests: 5
  • Passed: 5 ✅
  • Failed: 0
  • Skipped: 0
  • Duration: 5.18 min

✅ All smoke tests passed!

All critical flows are working correctly.


This is a non-blocking smoke test. Full regression tests run separately.

devin-ai-integration[bot]

This comment was marked as resolved.

…ptions, add init recovery

- Parent.ts: extract sendActionWithTimeoutRecovery so recoverable-error
  retry is outside the try-catch, preventing double-retry (max 1 reload
  per recovery path)
- ncs-signer.ts: split into DEFAULT_EVENT_OPTIONS (timeout only) and
  POLLING_EVENT_OPTIONS (timeout + intervalMs) — only get-status uses
  polling; side-effectful ops (OTP, sign, export) are never retried
- CrossmintWalletProvider.tsx: initializeWebView now reloads WebView and
  re-polls if first handshake fails, instead of throwing immediately
- Add tests for WebViewParent recovery (5 cases) and signer options (3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jcurbelo
Copy link
Copy Markdown
Contributor Author

jcurbelo commented Mar 9, 2026

Closing - this PR addresses too many concerns at once. Will break it into smaller, focused PRs starting with the isConnected gate fix.

@jcurbelo jcurbelo closed this Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant