Skip to content

Commit 90bd184

Browse files
committed
Merge origin/main into stable-main-7.61.0
2 parents 52de212 + cc4e96b commit 90bd184

File tree

2,731 files changed

+250137
-78251
lines changed

Some content is hidden

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

2,731 files changed

+250137
-78251
lines changed

.cursor/BUGBOT.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# BUGBOT Rules
2+
3+
## Core Mission
4+
5+
Automated test quality enforcement and bug detection for MetaMask Mobile React Native codebase
6+
7+
## Execution Protocol
8+
9+
### 1. Initial Setup
10+
11+
- **ALWAYS** load and reference [unit testing guidelines](rules/unit-testing-guidelines.mdc)
12+
- Verify test file naming pattern: `*.test.{ts,tsx,js,jsx}`
13+
- Check for proper Jest/React Native Testing Library imports
14+
15+
Use the rules in the[unit testing guidelines](rules/unit-testing-guidelines.mdc) to enforce the test quality and bug detection.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
globs: "**/*"
3+
alwaysApply: true
4+
---
5+
6+
# Feature Flag Guidelines
7+
8+
## Core Principle
9+
10+
**ALWAYS** use the `useFeatureFlag` hook instead of creating new feature flag selectors.
11+
12+
## Forbidden Patterns
13+
14+
### ❌ NEVER Create New Feature Flag Selectors
15+
16+
**DO NOT** create new selectors using `createSelector` for feature flags:
17+
18+
```typescript
19+
// ❌ FORBIDDEN - Do not create new feature flag selectors
20+
export const selectMyFeatureEnabledFlag = createSelector(
21+
selectRemoteFeatureFlags,
22+
(remoteFeatureFlags) => {
23+
// ... selector logic
24+
},
25+
);
26+
```
27+
28+
**DO NOT** add new feature flag selectors in:
29+
- `app/selectors/featureFlagController/**/*.ts`
30+
- `app/components/**/selectors/featureFlags/**/*.ts`
31+
- Any other location that creates feature flag selectors
32+
33+
## Required Pattern
34+
35+
### ✅ ALWAYS Use the `useFeatureFlag` Hook
36+
37+
**MUST** use the `useFeatureFlag` hook from `app/components/hooks/FeatureFlags/useFeatureFlag.ts`:
38+
39+
```typescript
40+
// ✅ REQUIRED - Use the hook instead
41+
import { useFeatureFlag, FeatureFlagNames } from '../../../hooks/FeatureFlags/useFeatureFlag';
42+
43+
const MyComponent = () => {
44+
const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.rewardsEnabled);
45+
46+
// Use the flag value
47+
if (isFeatureEnabled) {
48+
// ... feature logic
49+
}
50+
};
51+
```
52+
53+
## Steps to Use Feature Flags
54+
55+
1. **Add the flag name** to the `FeatureFlagNames` enum in `app/components/hooks/FeatureFlags/useFeatureFlag.ts`:
56+
```typescript
57+
export enum FeatureFlagNames {
58+
rewardsEnabled = 'rewardsEnabled',
59+
myNewFeature = 'myNewFeature', // Add your new flag here
60+
}
61+
```
62+
63+
2. **Use the hook** in your component:
64+
```typescript
65+
const isMyFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature);
66+
```
67+
68+
3. **Do NOT** create a selector for the feature flag
69+
70+
## Migration Pattern
71+
72+
If you encounter existing feature flag selectors, prefer migrating to the hook:
73+
74+
```typescript
75+
// ❌ Old pattern (existing code - do not replicate)
76+
const isFeatureEnabled = useSelector(selectMyFeatureEnabledFlag);
77+
78+
// ✅ New pattern (use this instead)
79+
const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature);
80+
```
81+
82+
## Enforcement
83+
84+
- **REJECT** any code that creates new `createSelector` instances for feature flags
85+
- **REJECT** any new files in `app/selectors/featureFlagController/` directories
86+
- **REQUIRE** use of `useFeatureFlag` hook for all feature flag access
87+
- **REQUIRE** adding flag names to `FeatureFlagNames` enum before use
88+
89+
## Exception
90+
91+
The only exception is the base selector `selectRemoteFeatureFlags` in `app/selectors/featureFlagController/index.ts`, which is used internally by the `useFeatureFlag` hook infrastructure.

.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` • `ppom/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`
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,92 @@ 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+
188274
# Reviewer Responsibilities
189275

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

219306
# Common Mistakes to AVOID - CRITICAL
220307

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

230318
# Anti-patterns to Avoid
231319

.depcheckrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ 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'
2527
# Remove this once it's used in the project
2628
- 'rive-react-native'
2729
# Appwright will be used in a follow up PR. We will remove once PR is ready
@@ -68,6 +70,7 @@ ignores:
6870
- 'prettier-plugin-gherkin'
6971
- 'react-native-svg-asset-plugin'
7072
- 'regenerator-runtime'
73+
- 'prettier-2'
7174

7275
## Unused devDependencies to investigate
7376
- '@metamask/swappable-obj-proxy'

.detoxrc.js

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module.exports = {
2222
$0: 'jest',
2323
config: 'e2e/jest.e2e.config.js',
2424
},
25-
detached: true,
25+
detached: process.env.CI ? true : false,
2626
jest: {
2727
setupTimeout: 220000,
2828
teardownTimeout: 60000, // Increase teardown timeout from default 30s to 60s
@@ -65,8 +65,12 @@ module.exports = {
6565
},
6666
'ios.sim.main.ci': {
6767
device: 'ios.simulator',
68-
app: 'ios.debug',
69-
}
68+
app: 'ios.main.release',
69+
},
70+
'ios.sim.flask.ci': {
71+
device: 'ios.simulator',
72+
app: 'ios.flask.release',
73+
},
7074
},
7175
devices: {
7276
'ios.simulator': {
@@ -110,42 +114,44 @@ module.exports = {
110114
'ios.main.release': {
111115
type: 'ios.app',
112116
binaryPath:
113-
'ios/build/Build/Products/Release-iphonesimulator/MetaMask.app',
114-
build: `yarn build:ios:main:e2e`,
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`,
115119
},
116120
'ios.flask.debug': {
117121
type: 'ios.app',
118122
binaryPath:
119-
'ios/build/Build/Products/Debug-iphonesimulator/MetaMask-Flask.app',
120-
build: 'yarn start:ios:e2e:flask',
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',
121125
},
122126
'ios.flask.release': {
123127
type: 'ios.app',
124128
binaryPath:
125-
'ios/build/Build/Products/Release-iphonesimulator/MetaMask-Flask.app',
126-
build: `yarn build:ios:flask:e2e`,
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`,
127131
},
128132
'android.debug': {
129133
type: 'android.apk',
130134
binaryPath: process.env.PREBUILT_ANDROID_APK_PATH || 'android/app/build/outputs/apk/prod/debug/app-prod-debug.apk',
131-
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH,
132-
build: 'yarn start:android:e2e',
135+
testBinaryPath: process.env.PREBUILT_ANDROID_TEST_APK_PATH || 'android/app/build/outputs/apk/androidTest/prod/debug/app-prod-debug-androidTest.apk',
136+
build: 'export CONFIGURATION="Debug" && yarn build:android:main:e2e',
133137
},
134-
'android.flask.debug': {
138+
'android.release': {
135139
type: 'android.apk',
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',
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 || 'android/app/build/outputs/apk/androidTest/prod/release/app-prod-release-androidTest.apk',
142+
build: `export CONFIGURATION="Release" && yarn build:android:main:e2e`,
139143
},
140-
'android.release': {
144+
'android.flask.debug': {
141145
type: 'android.apk',
142-
binaryPath: 'android/app/build/outputs/apk/prod/release/app-prod-release.apk',
143-
build: `yarn build:android:main:e2e`,
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 || 'android/app/build/outputs/apk/androidTest/flask/debug/app-flask-debug-androidTest.apk',
148+
build: 'export CONFIGURATION="Debug" && yarn build:android:flask:e2e',
144149
},
145150
'android.flask.release': {
146151
type: 'android.apk',
147-
binaryPath: 'android/app/build/outputs/apk/flask/release/app-flask-release.apk',
148-
build: `yarn build:android:flask:e2e`,
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 || 'android/app/build/outputs/apk/androidTest/flask/release/app-flask-release-androidTest.apk',
154+
build: `export CONFIGURATION="Release" && yarn build:android:flask:e2e`,
149155
},
150156
},
151157
};

.eslintignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ node_modules
1010
CHANGELOG.md
1111
wdio
1212
/app/util/test/testSetup.js
13-
/app/lib/ppom/ppom.html.js
14-
/app/lib/ppom/blockaid-version.js
15-
/ppom
1613
junitProperties.js
1714
/e2e/scripts/ai-e2e-tags-selector.ts
18-
scripts/aggregate-performance-reports.mjs
15+
scripts/aggregate-performance-reports.mjs

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ module.exports = {
7777
},
7878
},
7979
{
80-
files: ['scripts/**/*.js', 'app.config.js'],
80+
files: ['scripts/**/*.js', 'e2e/tools/**/*.{js,ts}', 'app.config.js'],
8181
rules: {
8282
'no-console': 0,
8383
'import/no-commonjs': 0,

0 commit comments

Comments
 (0)