Skip to content

feat(rn-window): auto-recover on sendAction when WebView handshake failed#1599

Closed
panosinthezone wants to merge 3 commits intomainfrom
devin/1771615903-rn-webview-sendaction-auto-recovery
Closed

feat(rn-window): auto-recover on sendAction when WebView handshake failed#1599
panosinthezone wants to merge 3 commits intomainfrom
devin/1771615903-rn-webview-sendaction-auto-recovery

Conversation

@panosinthezone
Copy link
Copy Markdown
Contributor

@panosinthezone panosinthezone commented Feb 20, 2026

Description

When the React Native WebView handshake fails (timeout or error), subsequent sendAction calls now automatically reload the WebView and retry the handshake before sending the action. This makes the RN path self-healing, similar to how the web iframe path re-creates the iframe on failure.

The change adds a single guard at the top of WebViewParent.sendAction: if this.isConnected is false, it calls the existing reloadAndHandshake() (which already uses a single-flight pattern) before proceeding.

Human review checklist

  • Verify isConnected semantics: Confirm there are no transient states where isConnected is false but a reload would be counterproductive (e.g., during initial load before the first handshake completes). Note that onWebViewLoad in the provider sets parent.isConnected = false before calling handshakeWithChild(), and that call is not routed through reloadAndHandshake(), so the single-flight pattern won't deduplicate an early sendAction against the initial handshake.
  • Double-reload edge case: If isConnected is false and the subsequent action returns a recoverable error, reloadAndHandshake() would be called twice in sequence. Verify this is acceptable or unreachable in practice.
  • Concurrent sendAction calls: Multiple calls arriving while disconnected will all await reloadAndHandshake(). The single-flight pattern should coalesce them, but worth confirming.

Link to Devin run: https://crossmint.devinenterprise.com/sessions/2a0eb82cef7b442caaeac0ff1a9e2331
Requested by: @panosinthezone

Test plan

  • No new tests added. This is a behavioral change that relies on the existing reloadAndHandshake single-flight logic. Manual testing on a React Native app where the initial handshake times out would confirm the auto-recovery path.

Package updates

  • @crossmint/client-sdk-rn-window — patch changeset added (rn-webview-auto-recovery.md).

Open with Devin

…iled

When the React Native WebView handshake fails (timeout or error),
subsequent sendAction calls now automatically reload the WebView and
retry the handshake before sending the action. This makes the RN path
self-healing, similar to how the web iframe path re-creates the iframe
on failure.

Co-Authored-By: Panayiotis Halios <panos@paella.dev>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 20, 2026

🦋 Changeset detected

Latest commit: c5e3619

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

This PR includes changesets to release 3 packages
Name Type
@crossmint/client-sdk-rn-window Patch
expo-demo Patch
@crossmint/client-sdk-react-native-ui 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

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Original prompt from Panayiotis
## Goal
When the React Native WebView handshake fails (timeout or error), subsequent `sendAction` calls should automatically reload the WebView and retry the handshake before sending the action, similar to how the web iframe path re-creates the iframe on failure.

## Changes

### 1. `packages/client/rn-window/src/rn-webview/Parent.ts` — Override `sendAction` to handle disconnected state

Modify the `sendAction` override in `WebViewParent` to check `this.isConnected` before sending. If not connected, call `reloadAndHandshake()` first, then proceed with the action. This reuses the existing `reloadAndHandshake()` method which already has the single-flight pattern.

The updated `sendAction` should look like:

```typescript
public override async sendAction<K extends keyof OutgoingEvents, R extends keyof IncomingEvents>(
    args: SendActionArgs<IncomingEvents, OutgoingEvents, K, R>
): Promise<z.infer<IncomingEvents[R]>> {
    // If not connected (e.g., handshake timed out), reload and re-handshake before sending
    if (!this.isConnected) {
        console.info("[WebViewParent] Not connected, reloading and re-establishing handshake before sendAction");
        await this.reloadAndHandshake();
    }

    const response = await super.sendAction(args);

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

    return response;
}

Note: reloadAndHandshake() is currently private. Change it to protected or just keep it private since the override is within the same class — it's already accessible. No visibility change needed.

2. packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx — Optionally improve getClientTEEConnection

The current getClientTEEConnection only checks webViewParentRef.current == null. This is actually fine with the change ab... (742 chars truncated...)

</details>

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Co-Authored-By: Panayiotis Halios <panos@paella.dev>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +105 to +108
if (!this.isConnected) {
console.info("[WebViewParent] Not connected, reloading and re-establishing handshake before sendAction");
await this.reloadAndHandshake();
}
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.

Race condition during initial WebView load: if sendAction() is called after onWebViewLoad sets isConnected = false but before handshakeWithChild() completes, this will trigger an unnecessary reload.

The initial handshake is already in progress (via _ongoingHandshakeWithChild), but reloadAndHandshake() uses a separate single-flight guard (_reconnectFlight), so it will reload the WebView mid-handshake.

Consider checking if a handshake is already in progress before reloading:

Suggested change
if (!this.isConnected) {
console.info("[WebViewParent] Not connected, reloading and re-establishing handshake before sendAction");
await this.reloadAndHandshake();
}
if (!this.isConnected && this._ongoingHandshakeWithChild == null) {
console.info("[WebViewParent] Not connected, reloading and re-establishing handshake before sendAction");
await this.reloadAndHandshake();
} else if (!this.isConnected) {
console.info("[WebViewParent] Handshake already in progress, waiting for it to complete");
await this.handshakeWithChild();
}
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: 105-108

Comment:
Race condition during initial WebView load: if `sendAction()` is called after `onWebViewLoad` sets `isConnected = false` but before `handshakeWithChild()` completes, this will trigger an unnecessary reload.

The initial handshake is already in progress (via `_ongoingHandshakeWithChild`), but `reloadAndHandshake()` uses a separate single-flight guard (`_reconnectFlight`), so it will reload the WebView mid-handshake.

Consider checking if a handshake is already in progress before reloading:

```suggestion
        if (!this.isConnected && this._ongoingHandshakeWithChild == null) {
            console.info("[WebViewParent] Not connected, reloading and re-establishing handshake before sendAction");
            await this.reloadAndHandshake();
        } else if (!this.isConnected) {
            console.info("[WebViewParent] Handshake already in progress, waiting for it to complete");
            await this.handshakeWithChild();
        }
```

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

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Co-Authored-By: Panayiotis Halios <panos@paella.dev>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Comment on lines +105 to +108
if (!this.isConnected) {
try {
console.info("[WebViewParent] Not connected, attempting handshake before sendAction");
await this.handshakeWithChild();
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.

🚩 Unconditional reload on !isConnected regardless of recovery option

The new !isConnected check at line 105 triggers reloadAndHandshake() unconditionally, even when no recovery option is configured. In contrast, the error-code-based recovery path at line 112 is gated by this.recoveryOptions via isRecoverableError() (Parent.ts:85-86). The recovery option's JSDoc at Parent.ts:24-26 states: "When not provided, no automatic recovery is performed (backward compatible)." This documentation is now inaccurate since the !isConnected auto-recovery applies to all WebViewParent instances. This may be intentional (the changeset describes it as a general improvement), but the inconsistency with the documented contract is worth noting. Consider either gating the !isConnected recovery behind this.recoveryOptions != null, or updating the documentation to reflect the new unconditional behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@albertoelias-crossmint
Copy link
Copy Markdown
Collaborator

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.

2 participants