Skip to content

Commit c4bd9d2

Browse files
temp+fix: Android In-App Browser Crash (#22212)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** The app crashes on Android when users grant **camera + microphone** permissions through the in-app browser (e.g., visiting https://permission.site/). Root Cause: The WebView permission handling code had a fundamental architectural flaw when multiple permissions (camera + microphone) were requested simultaneously: Old behavior: Showed separate dialogs for each permission (one for camera, one for microphone) Each dialog's "Allow" button independently called request.grant() Android's WebView API only allows calling grant() or deny() once per permission request When the user clicked "Allow" on the second dialog → CRASH: "Either grant() or deny() has been already called" Solution: Rewrote the permission handling logic in RNCWebChromeClient.java: Changed permission handling to show ONE dialog for all permissions: - "Allow example.com to use your camera and microphone?" - Call grant() only once with all permissions together - Added error handling to prevent crashes in edge cases Android build for this PR: https://app.bitrise.io/build/560c02e8-611e-4bfd-a24c-34e69751c86b?tab=artifacts PR to browser: MetaMask/react-native-webview-mm#72 ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry:null ## **Related issues** Fixes: #22162 ## **Manual testing steps** ```gherkin Feature: Fix Android double permission bug on webview Scenario: App does not crash when user grant/deny camera + microphone permission Given user navigates to website https://permission.site in website When user select Camera + Microphone Then user is able give/deny permission to Camera and Microphone without app crashing ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** Android Before ![Android Before](https://github.com/user-attachments/assets/8e4ab52e-9464-47b6-90d5-2edc5fbfca2a) ### **After** Android After ![Android After](https://github.com/user-attachments/assets/f0b28c3a-24cb-447e-bcff-585104109d47) <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Consolidates Android WebView permission flow into a single dialog with a single grant call for multiple resources, adds error handling, and delivers via a Yarn patch of @metamask/react-native-webview. > > - **Android WebView (`RNCWebChromeClient.java`)**: > - Show a single confirmation dialog for multiple already-OS-granted permissions with a combined message. > - Aggregate requested resources and call `request.grant()` once; map `CAMERA` → `RESOURCE_VIDEO_CAPTURE`, `RECORD_AUDIO` → `RESOURCE_AUDIO_CAPTURE`, handle `RESOURCE_PROTECTED_MEDIA_ID`. > - Reset state before handling requests; manage `permissionRequest`/`grantedPermissions` lifecycle; only set `permissionRequest` when system permissions must be requested. > - Wrap `grant()` with `try/catch` to ignore `IllegalStateException` and prevent duplicate grant/deny errors. > - Minor UX: delay enabling the "Allow" button to avoid accidental taps. > - **Dependencies**: > - Use Yarn patch for `@metamask/react-native-webview@14.5.0` via `.yarn/patches/...`; update `package.json` and `yarn.lock`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7396b7f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: metamaskbot <metamaskbot@users.noreply.github.com>
1 parent d6ef94e commit c4bd9d2

File tree

1,959 files changed

+51903
-162716
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,959 files changed

+51903
-162716
lines changed

.cursor/rules/BUGBOT.md

Lines changed: 0 additions & 174 deletions
This file was deleted.

.cursor/rules/general-coding-guidelines.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ ComponentName/
3232

3333
**Core**: `/docs/readme/` (architecture, testing, debugging, performance, environment, expo-environment, storybook, troubleshooting, expo-e2e-testing, reassure, release-build-profiler)
3434

35-
**Features**: `/docs/` (deeplinks, animations, tailwind, confirmations, confirmation-refactoring) • `app/component-library/README.md` • `e2e/MOCKING.md` • `CHANGELOG.md` • `app/core/{Analytics,Engine}/README.md`
35+
**Features**: `/docs/` (deeplinks, animations, tailwind, confirmations, confirmation-refactoring) • `app/component-library/README.md` • `e2e/MOCKING.md` • `CHANGELOG.md` • `app/core/{Analytics,Engine}/README.md` • `ppom/README.md`
3636

3737
**External**: [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) • [TypeScript Guidelines](https://github.com/MetaMask/contributor-docs/blob/main/docs/typescript.md) • [Unit Testing](https://github.com/MetaMask/contributor-docs/blob/main/docs/testing/unit-testing.md)
3838

@@ -44,4 +44,4 @@ ComponentName/
4444

4545
**Testing**: see .claude/commands/unit-test.md
4646

47-
**Forbidden**: ❌ npm/npx commands
47+
**Forbidden**: ❌ npm/npx commands

.cursor/rules/unit-testing-guidelines.mdc

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -185,92 +185,6 @@ jest.setSystemTime(new Date('2024-01-01'));
185185

186186
- Avoid relying on global state or hardcoded values (e.g., dates) or mock it.
187187

188-
## Async Testing and act() - CRITICAL
189-
190-
**ALWAYS use `act()` when testing async operations that trigger React state updates.**
191-
192-
### When to Use act()
193-
194-
Use `act()` when you:
195-
- Call async functions via component props (e.g., `onRefresh`, `onPress` with async handlers)
196-
- Invoke functions that perform state updates asynchronously
197-
- Test pull-to-refresh or other async interactions
198-
199-
### Symptoms of Missing act()
200-
201-
Tests fail intermittently with:
202-
- `TypeError: terminated`
203-
- `SocketError: other side closed`
204-
- Warnings about state updates not being wrapped in act()
205-
206-
### Examples
207-
208-
```ts
209-
// ❌ WRONG - Causes flaky tests
210-
it('calls Logger.error when handleOnRefresh fails', async () => {
211-
const mockLoggerError = jest.spyOn(Logger, 'error');
212-
render(BankDetails);
213-
214-
// Async function called without act() - causes race condition
215-
screen
216-
.getByTestId('refresh-control-scrollview')
217-
.props.refreshControl.props.onRefresh();
218-
219-
await waitFor(() => {
220-
expect(mockLoggerError).toHaveBeenCalled();
221-
});
222-
});
223-
224-
// ✅ CORRECT - Properly handles async state updates
225-
it('calls Logger.error when handleOnRefresh fails', async () => {
226-
const mockLoggerError = jest.spyOn(Logger, 'error');
227-
render(BankDetails);
228-
229-
// Wrap async operation in act()
230-
await act(async () => {
231-
await screen
232-
.getByTestId('refresh-control-scrollview')
233-
.props.refreshControl.props.onRefresh();
234-
});
235-
236-
await waitFor(() => {
237-
expect(mockLoggerError).toHaveBeenCalled();
238-
});
239-
});
240-
```
241-
242-
### Common Patterns Requiring act()
243-
244-
```ts
245-
// RefreshControl callbacks
246-
await act(async () => {
247-
await refreshControl.props.onRefresh();
248-
});
249-
250-
// Async button press handlers
251-
await act(async () => {
252-
await button.props.onPress();
253-
});
254-
255-
// Any async callback that updates state
256-
await act(async () => {
257-
await component.props.onSomeAsyncAction();
258-
});
259-
```
260-
261-
### Why This Matters
262-
263-
Without `act()`:
264-
1. Async function starts executing
265-
2. Test continues and waits only for specific assertion
266-
3. Jest cleanup/termination happens while promises are still pending
267-
4. Results in "terminated" or "other side closed" errors
268-
269-
With `act()`:
270-
1. React Testing Library waits for all state updates
271-
2. All promises resolve before test proceeds
272-
3. Clean, deterministic test execution
273-
274188
# Reviewer Responsibilities
275189

276190
- Validate that tests fail when the code is broken (test the test).
@@ -301,7 +215,6 @@ Before submitting any test file, verify:
301215
- [ ] **Assertions are specific**
302216
- [ ] **Test names are descriptive**
303217
- [ ] **No test duplication**
304-
- [ ] **Async operations wrapped in act()** when they trigger state updates
305218

306219
# Common Mistakes to AVOID - CRITICAL
307220

@@ -313,7 +226,6 @@ Before submitting any test file, verify:
313226
- ❌ **Not following AAA pattern** - Always Arrange, Act, Assert
314227
- ❌ **Not testing edge cases** - Test null, undefined, empty values
315228
- ❌ **Using weak matchers** - Use specific assertions like `toBe()`, `toEqual()`
316-
- ❌ **Not wrapping async state updates in act()** - Causes flaky "terminated" errors
317229

318230
# Anti-patterns to Avoid
319231

.depcheckrc.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ ignores:
2222
- 'reflect-metadata'
2323
# Husky is used for pre-commit hooks
2424
- 'husky'
25-
# Auto changelog tooling used in release scripts
26-
- '@metamask/auto-changelog'
2725
# Remove this once it's used in the project
2826
- 'rive-react-native'
2927
# Appwright will be used in a follow up PR. We will remove once PR is ready
@@ -70,7 +68,6 @@ ignores:
7068
- 'prettier-plugin-gherkin'
7169
- 'react-native-svg-asset-plugin'
7270
- 'regenerator-runtime'
73-
- 'prettier-2'
7471

7572
## Unused devDependencies to investigate
7673
- '@metamask/swappable-obj-proxy'

.detoxrc.js

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,8 @@ module.exports = {
6565
},
6666
'ios.sim.main.ci': {
6767
device: 'ios.simulator',
68-
app: 'ios.main.release',
69-
},
70-
'ios.sim.flask.ci': {
71-
device: 'ios.simulator',
72-
app: 'ios.flask.release',
73-
},
68+
app: 'ios.debug',
69+
}
7470
},
7571
devices: {
7672
'ios.simulator': {
@@ -114,44 +110,42 @@ module.exports = {
114110
'ios.main.release': {
115111
type: 'ios.app',
116112
binaryPath:
117-
process.env.PREBUILT_IOS_APP_PATH || 'ios/build/Build/Products/Release-iphonesimulator/MetaMask.app',
118-
build: `export CONFIGURATION="Release" && yarn build:ios:main:e2e`,
113+
'ios/build/Build/Products/Release-iphonesimulator/MetaMask.app',
114+
build: `yarn build:ios:main:e2e`,
119115
},
120116
'ios.flask.debug': {
121117
type: 'ios.app',
122118
binaryPath:
123-
process.env.PREBUILT_IOS_APP_PATH || 'ios/build/Build/Products/Debug-iphonesimulator/MetaMask-Flask.app',
124-
build: 'export CONFIGURATION="Debug" && yarn build:ios:flask:e2e',
119+
'ios/build/Build/Products/Debug-iphonesimulator/MetaMask-Flask.app',
120+
build: 'yarn start:ios:e2e:flask',
125121
},
126122
'ios.flask.release': {
127123
type: 'ios.app',
128124
binaryPath:
129-
process.env.PREBUILT_IOS_APP_PATH || 'ios/build/Build/Products/Release-iphonesimulator/MetaMask-Flask.app',
130-
build: `export CONFIGURATION="Release" && yarn build:ios:flask:e2e`,
125+
'ios/build/Build/Products/Release-iphonesimulator/MetaMask-Flask.app',
126+
build: `yarn build:ios:flask:e2e`,
131127
},
132128
'android.debug': {
133129
type: 'android.apk',
134130
binaryPath: process.env.PREBUILT_ANDROID_APK_PATH || 'android/app/build/outputs/apk/prod/debug/app-prod-debug.apk',
135131
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH,
136-
build: 'export CONFIGURATION="Debug" && yarn build:android:main:e2e',
132+
build: 'yarn start:android:e2e',
137133
},
138-
'android.release': {
134+
'android.flask.debug': {
139135
type: 'android.apk',
140-
binaryPath: process.env.PREBUILT_ANDROID_APK_PATH || 'android/app/build/outputs/apk/prod/release/app-prod-release.apk',
141-
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH,
142-
build: `export CONFIGURATION="Release" && yarn build:android:main:e2e`,
136+
binaryPath: 'android/app/build/outputs/apk/flask/debug/app-flask-debug.apk',
137+
testBinaryPath: 'android/app/build/outputs/apk/androidTest/flask/debug/app-flask-debug-androidTest.apk',
138+
build: 'yarn start:android:e2e:flask',
143139
},
144-
'android.flask.debug': {
140+
'android.release': {
145141
type: 'android.apk',
146-
binaryPath: process.env.PREBUILT_ANDROID_APK_PATH || 'android/app/build/outputs/apk/flask/debug/app-flask-debug.apk',
147-
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH,
148-
build: 'export CONFIGURATION="Debug" && yarn build:android:flask:e2e',
142+
binaryPath: 'android/app/build/outputs/apk/prod/release/app-prod-release.apk',
143+
build: `yarn build:android:main:e2e`,
149144
},
150145
'android.flask.release': {
151146
type: 'android.apk',
152-
binaryPath: process.env.PREBUILT_ANDROID_APK_PATH || 'android/app/build/outputs/apk/flask/release/app-flask-release.apk',
153-
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH,
154-
build: `export CONFIGURATION="Release" && yarn build:android:flask:e2e`,
147+
binaryPath: 'android/app/build/outputs/apk/flask/release/app-flask-release.apk',
148+
build: `yarn build:android:flask:e2e`,
155149
},
156150
},
157151
};

0 commit comments

Comments
 (0)