Skip to content

fix: make in-app webview back button tappable on iOS#29693

Merged
wachunei merged 4 commits into
mainfrom
fix/simple-webview-header
May 5, 2026
Merged

fix: make in-app webview back button tappable on iOS#29693
wachunei merged 4 commits into
mainfrom
fix/simple-webview-header

Conversation

@tommasini

@tommasini tommasini commented May 4, 2026

Copy link
Copy Markdown
Contributor

Description

After the React Navigation v5 → v6 migration (#26691) and the follow-up iOS overlap fix (#29020), the back arrow in the in-app SimpleWebview (used e.g. when tapping "View on block explorer" after a swap) renders behind the iOS status bar / Dynamic Island and is unclickable, leaving the user trapped on the screen.

Root cause:

  • SimpleWebview was injecting its header through navigation.setOptions({ header: () => <HeaderCompactStandard …/> }).
  • In @react-navigation/stack v6, custom header functions do not receive automatic paddingTop: headerStatusBarHeight (see node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx); the custom header must own its safe-area inset.
  • The previous iOS overlap fix worked around that by setting includesTopInset: Device.isAndroid(), which on iOS resulted in no top safe-area inset at all, so the back button rendered at y = 0 underneath the status bar.

Solution (matches the pattern already used by WebviewModal.tsx in Deposit):

  • Render HeaderCompactStandard directly inside SimpleWebview with includesTopInset always on, so useSafeAreaInsets() (already used by HeaderBase) drives the top padding consistently on both platforms.
  • Hide the inner Stack.Navigator headers in both webview wrappers (MainNavigator.js and App.tsx) via screenOptions={{ headerShown: false }} so we don't double-render headers.
  • Remove the brittle setOptions custom-header wiring; this also removes the need for the Device.isAndroid() platform branch and the rounded-t-2xl styling that was masking the issue.

The navigation contract used by callers (navigation.navigate(Routes.WEBVIEW.MAIN, { screen: Routes.WEBVIEW.SIMPLE, params: { url } })) is unchanged, so BridgeTransactionDetails, BlockExplorersModal, and other consumers keep working as-is.

Longer-term, the team has discussed routing all external links through a single service that selects between WebView and inAppReborn (Pedro / Joao in the originating Slack thread); that refactor is intentionally out of scope here — this PR just unblocks the user.

Changelog

CHANGELOG entry: Fixed an iOS bug where the back button in the in-app webview (e.g. "View on block explorer") was rendered behind the status bar and could not be tapped.

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-572

bug surfaced in the #metamask-core-mobile-ux Slack thread on 2026-04-28 (browser back button unclickable after swap → "View on block explorer")

Manual testing steps

```gherkin
Feature: In-app webview back button is reachable on iOS

Scenario: User exits the in-app webview opened from a swap transaction
Given the user has completed a swap on iOS
And the user is on the swap transaction details screen

When the user taps "View on block explorer"
Then the in-app webview opens with the block explorer page
And the header is rendered fully below the status bar / Dynamic Island
And the back arrow in the top-left of the header is tappable

When the user taps the back arrow
Then the user returns to the previous screen

Scenario: Header still renders correctly on Android
Given the user is on Android
When the user opens any flow that pushes Routes.WEBVIEW.MAIN / Routes.WEBVIEW.SIMPLE
Then the header is rendered with the correct top safe-area inset
And the share button on the right works
And the back button on the left works

Scenario: Header still renders correctly when the webview is presented as a modal during onboarding
Given the user is in the onboarding flow on iOS
When a screen pushes Routes.WEBVIEW.MAIN (modal presentation)
Then the in-app webview opens with the header below the status bar
And the back arrow is tappable and returns to the onboarding screen
```

Screenshots/Recordings

Screen.Recording.2026-05-04.at.23.18.02.mov

Before

iOS: back arrow rendered behind the status bar / Dynamic Island and not tappable — user is trapped on the webview screen (see Slack screenshot from Heyse Li, 2026-04-28).

After

iOS: header renders below the status bar with proper safe-area inset; back arrow is fully visible and tappable.

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Made with Cursor


Note

Low Risk
Low risk UI/navigation change that primarily adjusts header rendering for SimpleWebview; main risk is unintended header/spacing differences or missing headers in the webview stacks.

Overview
Fixes an iOS layout regression where the in-app SimpleWebview back button could be obscured by the status bar/Dynamic Island by rendering HeaderCompactStandard directly inside SimpleWebview (always includesTopInset) instead of injecting a custom header via navigation.setOptions().

Updates the webview stack wrappers in MainNavigator and App to hide the React Navigation header (screenOptions={{ headerShown: false }}) to avoid double headers, and adjusts tests to validate the new header rendering and callbacks (back + share).

Reviewed by Cursor Bugbot for commit 972444a. Bugbot is set up for automated code reviews on this repo. Configure here.

@tommasini tommasini self-assigned this May 4, 2026
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2 metamaskbotv2 Bot added the team-mobile-platform Mobile Platform team label May 4, 2026
@github-actions github-actions Bot added the size-M label May 4, 2026
@github-project-automation github-project-automation Bot moved this to Needs dev review in PR review queue May 4, 2026
@tommasini tommasini marked this pull request as ready for review May 4, 2026 22:38

@Cal-L Cal-L left a comment

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.

LGTM

@github-project-automation github-project-automation Bot moved this from Needs dev review to Review finalised - Ready to be merged in PR review queue May 4, 2026

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1f9720f. Configure here.

() => ({
__esModule: true,
default: jest.fn(() => ({
header: () => null,

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.

Test missing mock for useTailwind dependency

Medium Severity

The component now calls useTailwind() from @metamask/design-system-twrnc-preset, but the test file never mocks this module. There is no global mock for it in testSetup.js or app/__mocks__/. Every other test file in the codebase that renders components using useTailwind adds an explicit jest.mock('@metamask/design-system-twrnc-preset', () => ({ useTailwind: () => ({ style: () => ({}) }) })). Without this mock, the test will crash when useTailwind() is invoked without a provider context.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f9720f. Configure here.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeStake, SmokeMoney, SmokeSwap, SmokeWalletPlatform, SmokeAccounts, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR refactors the SimpleWebview component's header rendering approach:

  1. App.tsx & MainNavigator.js: Added screenOptions={{ headerShown: false }} to both Stack.Navigators that host SimpleWebview. This is necessary because the header is now rendered inline rather than via navigation options.

  2. SimpleWebview/index.tsx: Moved from navigation.setOptions() with getHeaderCompactStandardNavbarOptions to rendering HeaderCompactStandard directly as a JSX component inside a View. Also always sets includesTopInset: true (previously was Android-conditional).

Risk: The SimpleWebview is used across many features as a fallback browser for external links. The header change could affect:

  • Visual rendering (header appearance, safe area insets on iOS/Android)
  • Navigation behavior (back button, share button)
  • Any flow that navigates to SimpleWebview

Affected areas by feature:

  • SmokePerps: PerpsHomeView and transaction views navigate to SimpleWebview for external links
  • SmokeStake: LearnMoreModal and StakingConfirmation LegalLinks use SimpleWebview
  • SmokeMoney: Ramp ProcessingInfoModal, SettingsModal, ErrorDetailsModal, OrderContent all fall back to SimpleWebview
  • SmokeSwap: Bridge TransactionDetails and BlockExplorersModal use SimpleWebview
  • SmokeWalletPlatform: Transaction history (TransactionElement) and Perps section in Trending use SimpleWebview
  • SmokeAccounts: RevealSRP sheet navigates to SimpleWebview for help links
  • SmokeConfirmations: Required as dependent tag for SmokePerps, SmokeStake, SmokeSwap, SmokeMoney flows

The change is relatively contained (UI refactor, not logic change) but touches a widely-used shared component, warranting medium risk and broad coverage.

Performance Test Selection:
The changes are a UI refactor of the SimpleWebview header rendering approach - moving from navigation.setOptions() to inline JSX rendering. This does not affect app startup, account list rendering, login flows, swap performance, or any performance-critical paths. No performance tests are warranted.

View GitHub Actions results

@sonarqubecloud

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown

@wachunei wachunei added this pull request to the merge queue May 5, 2026
Merged via the queue into main with commit ee27c28 May 5, 2026
105 of 107 checks passed
@wachunei wachunei deleted the fix/simple-webview-header branch May 5, 2026 16:41
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.77.0 Issue or pull request that will be included in release 7.77.0 label May 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.77.0 Issue or pull request that will be included in release 7.77.0 size-M team-mobile-platform Mobile Platform team

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants