Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions .cursor/rules/pr-creation-guidelines.mdc
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
description: Pull Request (PR) Creation Guidelines. Use these rules when creating pull requests
alwaysApply: false
---

# MetaMask Mobile Pull Request (PR) Creation Guidelines

These rules apply whenever creating PRs in the MetaMask Mobile repository or suggesting PR creation steps to others. Follow them to ensure consistent quality and smooth reviews.
Expand Down Expand Up @@ -85,11 +90,44 @@ CHANGELOG entry: null
- **Always assign the PR to yourself (the author)** immediately after creation.
- This ensures proper ownership tracking and notifications.

### Automatic Team Label Detection

When creating PRs, automatically detect and apply the correct team label based on:

1. **Author's GitHub team membership** - Check which MetaMask teams the author belongs to
2. **Available team labels in the repository** - Match against existing `team-*` labels
3. **Context awareness** - Consider files changed when author belongs to multiple teams

#### Generic Team Label Detection Process

```bash
# Step 1: Get author's MetaMask teams
USER_TEAMS=$(gh api user/teams --paginate | jq -r '.[] | select(.organization.login == "MetaMask") | .slug')

# Step 2: Get available team labels in the repository
REPO_TEAM_LABELS=$(gh label list --search "team-" --limit 100 | cut -f1)

# Step 3: Try to find a matching team label
# Common mappings to check:
# - Exact match: team-{github-team} (e.g., mobile-platform → team-mobile-platform)
# - Without suffix: design-system-engineers → team-design-system
# - With wallet prefix: wallet-ux → team-wallet-ux

# If no clear match is found, DO NOT add a team label
# It's better to have no team label than an incorrect one
```

### Required Labels

Apply labels to enable automation and proper routing. Some labels can block merging.

- **Team labels** (for internal devs): `team-mobile-ux`, `team-mobile-platform`, etc.
- **Team labels** (OPTIONAL - only add if there's a clear match):
- Auto-detect based on author's GitHub team membership when possible
- Follow the pattern `team-*`
- Only add if you can confidently match the GitHub team to a label
- If unsure, leave it out - no team label is better than a wrong one
- Check for deprecated labels in the label description
- To see all available team labels: `gh label list --search "team-" --limit 100`
- **QA labels**:
- `needs-qa` (blocks merging until QA passes)
- `No QA Needed`
Expand All @@ -102,10 +140,15 @@ Apply labels to enable automation and proper routing. Some labels can block merg
### Label and Assignment DO / DON'T

- ✅ DO: Assign the PR to yourself as the author
- ✅ DO: Try to auto-detect team label based on GitHub team membership
- ✅ DO: Leave team label empty if no clear match exists
- ✅ DO: Verify team labels aren't deprecated (check label descriptions)
- ✅ DO: Add a QA label for every PR
- ✅ DO: Add the correct team label for routing
- ✅ DO: Remove blocking labels when conditions are resolved
- ❌ DON'T: Leave PR unassigned
- ❌ DON'T: Force a team label if there's no clear match - it's optional
- ❌ DON'T: Use deprecated team labels (check label descriptions for deprecation notices)
- ❌ DON'T: Guess team labels - only use ones you're confident about
- ❌ DON'T: Merge while `needs-qa` or `DO-NOT-MERGE` is present

## 4. Branch Naming Conventions
Expand Down Expand Up @@ -153,13 +196,35 @@ Use descriptive branch names prefixed by the commit type.

## Examples for MetaMask Mobile Context

### Example with Auto-Detected Team Label

- Title: `feat: add NFT gallery to collectibles tab`
- Branch: `feat/add-nft-gallery`
- Assignee: The PR author (yourself)
- Labels: `team-mobile-ux`, `needs-qa`, `Run E2E Smoke`
- Team label: `team-design-system` (IF clear match found) or none (if no match)
- Required Labels: `needs-qa` or `No QA Needed`, plus E2E label
- Changelog: `Added NFT gallery to Collectibles tab.`
- Manual testing (Gherkin): see example above

### GitHub CLI Command Example

```bash
# Create PR with auto team detection
gh pr create \
--title "feat: add NFT gallery to collectibles tab" \
--assignee @me \
--draft

# Try to detect team label (optional)
USER_TEAMS=$(gh api user/teams --paginate | jq -r '.[] | select(.organization.login == "MetaMask") | .slug')

# Only add team label if there's a confident match
# Examples of clear matches:
# - mobile-platform → team-mobile-platform ✓
# - wallet-ux → team-wallet-ux ✓
# If no clear match, skip the team label
```

## Enforcement

- PRs must use `.github/pull-request-template.md` with all sections completed.
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ jobs:
else
echo "No changes detected"
fi
e2e-tests:
permissions:
contents: read
id-token: write
uses: ./.github/workflows/run-e2e.yml
secrets: inherit
merge-unit-tests:
runs-on: ubuntu-latest
needs: unit-tests
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/run-e2e-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
setup-simulator: ${{ inputs.platform == 'ios' }}
android-avd-name: emulator
configure-keystores: false
sd-card-size: 8192

- name: Build Detox framework cache (iOS)
if: ${{ inputs.platform == 'ios' }}
Expand Down Expand Up @@ -181,7 +182,10 @@ jobs:
if: failure()
run: |
echo "Removing APKs from artifacts..."
find artifacts/ -name "*app-*" -delete 2>/dev/null || true
# Remove APKs, but ignore coverage folders and ios-bundle
find artifacts/ \( -type d \( -name "coverage-*" -o -name "ios-bundle" \) -prune \) -o -type f -name "*app-*" -delete 2>/dev/null || true
# Also remove artifact directories that contain APK/AAB files (download-artifact creates directories)
find artifacts/ \( -type d \( -name "coverage-*" -o -name "ios-bundle" \) -prune \) -o -type d -name "*app-*" -exec rm -rf {} + 2>/dev/null || true
continue-on-error: true

- name: Upload screenshots
Expand Down
29 changes: 25 additions & 4 deletions .github/workflows/run-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ name: Mobile E2E Smoke Tests
on:
workflow_call:
workflow_dispatch:
pull_request:
types: [opened, synchronize]

permissions:
contents: write
contents: read
id-token: write

jobs:
Expand Down Expand Up @@ -198,6 +196,27 @@ jobs:
test_suite_tag: ".*Performance.*"
secrets: inherit

smoke-card-ios:
name: "Card Smoke (iOS)"
if: false
uses: ./.github/workflows/run-e2e-workflow.yml
with:
test-suite-name: smoke-card-ios
platform: ios
test_suite_tag: ".*SmokeCard.*"
secrets: inherit

smoke-card-android:
name: "Card Smoke (Android)"
needs: build-android-apks
if: always() && needs.build-android-apks.result != 'failure'
uses: ./.github/workflows/run-e2e-workflow.yml
with:
test-suite-name: smoke-card-android
platform: android
test_suite_tag: ".*SmokeCard.*"
secrets: inherit

smoke-api-specs:
name: "API Specs E2E"
if: false
Expand All @@ -207,7 +226,7 @@ jobs:
report-e2e-smoke-tests:
name: Report E2E Smoke Tests
runs-on: ubuntu-latest
if: false
if: always()
needs:
- smoke-confirmations-ios
- smoke-confirmations-android
Expand All @@ -226,6 +245,8 @@ jobs:
- smoke-network-expansion-android
- smoke-performance-ios
- smoke-performance-android
- smoke-card-ios
- smoke-card-android
- smoke-api-specs

steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ To learn how to contribute to the MetaMask codebase, visit our [Contributor Docs
- [Testing](./docs/readme/testing.md)
- [Debugging](./docs/readme/debugging.md)
- [Performance](./docs/readme/performance.md)
- [Release Build Profiling](./docs/readme/release-build-profiler.md)
- [API Call Logging for Debugging](./docs/readme/api-logging.md)
- [Storybook](./docs/readme/storybook.md)
- [Miscellaneous](./docs/readme/miscellaneous.md)
Expand Down
9 changes: 0 additions & 9 deletions app/actions/wizard/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React from 'react';
import { AccountGroupObject } from '@metamask/account-tree-controller';
import { AccountCell } from './AccountCell';
import AccountCell from './AccountCell';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import { fireEvent } from '@testing-library/react-native';
import { createMockAccountGroup } from '../test-utils';

// Mock navigation
const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({ navigate: mockNavigate }),
}));

const mockAccountGroup = createMockAccountGroup(
'keyring:test-group/ethereum',
'Test Account Group',
Expand Down Expand Up @@ -56,4 +65,13 @@ describe('AccountCell', () => {
expect(getByText('Test Account Group')).toBeTruthy();
expect(getByText('$1234567890.00')).toBeTruthy();
});

it('navigates to account actions when menu button is pressed', () => {
const { getByTestId } = renderAccountCell();
const menuButton = getByTestId('multichain-account-cell-menu');
fireEvent.press(menuButton);
expect(mockNavigate).toHaveBeenCalledWith('MultichainAccountActions', {
accountGroup: mockAccountGroup,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AccountGroupObject } from '@metamask/account-tree-controller';
import React from 'react';
import React, { useCallback } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useStyles } from '../../../hooks';
import styleSheet from './AccountCell.styles';
import Text, { TextColor, TextVariant } from '../../../components/Texts/Text';
Expand All @@ -12,14 +13,22 @@ import {
} from '../../../../components/UI/Box/box.types';
import Icon, { IconName, IconSize } from '../../../components/Icons/Icon';
import { AccountCellIds } from '../../../../../e2e/selectors/MultichainAccounts/AccountCell.selectors';
import Routes from '../../../../constants/navigation/Routes';

interface AccountCellProps {
accountGroup: AccountGroupObject;
isSelected: boolean;
}

export const AccountCell = ({ accountGroup, isSelected }: AccountCellProps) => {
const AccountCell = ({ accountGroup, isSelected }: AccountCellProps) => {
const { styles } = useStyles(styleSheet, { isSelected });
const { navigate } = useNavigation();

const handleMenuPress = useCallback(() => {
navigate(Routes.MULTICHAIN_ACCOUNTS.ACCOUNT_CELL_ACTIONS, {
accountGroup,
});
}, [navigate, accountGroup]);

return (
<Box
Expand Down Expand Up @@ -62,6 +71,7 @@ export const AccountCell = ({ accountGroup, isSelected }: AccountCellProps) => {
<TouchableOpacity
testID={AccountCellIds.MENU}
style={styles.menuButton}
onPress={handleMenuPress}
>
<Icon
name={IconName.MoreVertical}
Expand All @@ -73,3 +83,5 @@ export const AccountCell = ({ accountGroup, isSelected }: AccountCellProps) => {
</Box>
);
};

export default React.memo(AccountCell);
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { AccountCell as default } from './AccountCell';
export { default } from './AccountCell';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { render, fireEvent } from '@testing-library/react-native';
import AccountListCell from './AccountListCell';
import { createMockAccountGroup } from '../../test-utils';

const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({ navigate: mockNavigate }),
}));

const mockAccountGroup = createMockAccountGroup(
'keyring:test-group/ethereum',
'Test Account',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import {
createMockInternalAccountsWithAddresses,
} from '../test-utils';

const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({ navigate: mockNavigate }),
}));

describe('MultichainAccountSelectorList', () => {
const mockOnSelectAccount = jest.fn();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { MultichainAddressRowProps } from './MultichainAddressRow.types';

export const SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS: MultichainAddressRowProps = {
chainId: 'eip155:1',
networkName: 'Ethereum Mainnet',
address: '0x1234567890123456789012345678901234567890',
};
import { IconName } from '../../../components/Icons/Icon';

export const MULTICHAIN_ADDRESS_ROW_TEST_ID = 'multichain-address-row';
export const MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID =
Expand All @@ -17,3 +12,27 @@ export const MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID =
'multichain-address-row-copy-button';
export const MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID =
'multichain-address-row-qr-button';

export const SAMPLE_ICONS = [
{
name: IconName.Copy,
callback: () => {
// Do nothing
},
testId: MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID,
},
{
name: IconName.QrCode,
callback: () => {
// Do nothing
},
testId: MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID,
},
];

export const SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS: MultichainAddressRowProps = {
chainId: 'eip155:1',
networkName: 'Ethereum Mainnet',
address: '0x1234567890123456789012345678901234567890',
icons: SAMPLE_ICONS,
};
Loading
Loading