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
18 changes: 9 additions & 9 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ app/selectors/cardController.ts @MetaMask/card

# Confirmation Team
app/components/Views/confirmations @MetaMask/confirmations
app/components/Views/confirmations/external/staking @MetaMask/confirmations @MetaMask/metamask-earn
app/components/Views/confirmations/external/staking @MetaMask/confirmations @MetaMask/earn
app/core/Engine/controllers/approval-controller @MetaMask/confirmations
app/core/Engine/controllers/gas-fee-controller @MetaMask/confirmations
app/core/Engine/controllers/signature-controller @MetaMask/confirmations
Expand Down Expand Up @@ -148,14 +148,14 @@ app/components/UI/TemplateRenderer @MetaMask/confirmations @MetaMask/core-plat


# Earn Team
app/components/UI/Stake @MetaMask/metamask-earn
app/core/Engine/controllers/earn-controller @MetaMask/metamask-earn
app/core/Engine/messengers/earn-controller-messenger @MetaMask/metamask-earn
app/selectors/earnController @MetaMask/metamask-earn
**/Earn/** @MetaMask/metamask-earn
**/earn/** @MetaMask/metamask-earn
**/Money/** @MetaMask/metamask-earn
**/money/** @MetaMask/metamask-earn
app/components/UI/Stake @MetaMask/earn
app/core/Engine/controllers/earn-controller @MetaMask/earn
app/core/Engine/messengers/earn-controller-messenger @MetaMask/earn
app/selectors/earnController @MetaMask/earn
**/Earn/** @MetaMask/earn
**/earn/** @MetaMask/earn
**/Money/** @MetaMask/earn
**/money/** @MetaMask/earn

# Rewards Team
app/core/Engine/controllers/rewards-controller @MetaMask/rewards
Expand Down
30 changes: 30 additions & 0 deletions .github/scripts/collect-qa-stats.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,35 @@ async function collectPerformanceTestCounts() {
return result;
}

// ---------------------------------------------------------------------------
// Feature flag E2E coverage
// Delegates to tests/feature-flags/feature-flag-coverage-report.ts (single source of truth).
// Requires `yarn install` in the workflow so ts-node is available.
// ---------------------------------------------------------------------------

const FF_REPORT_PATH = 'tests/artifacts/feature-flag-coverage-report.json';

async function collectFeatureFlagCoverage() {
console.log('[feature_flags] running coverage report via ts-node...');
execSync('yarn ts-node tests/feature-flags/feature-flag-coverage-report.ts', { stdio: 'pipe' });

const report = JSON.parse(await readFile(FF_REPORT_PATH, 'utf8'));
const { summary } = report;

console.log(`[feature_flags] total: ${summary.totalFlags}, active: ${summary.activeFlags}, coverage: ${summary.coveragePercentage}%`);

return {
total_flags: summary.totalFlags,
active_flags: summary.activeFlags,
deprecated_flags: summary.deprecatedFlags,
in_prod_flags: summary.inProdFlags,
full_coverage_flags: summary.fullCoverage,
partial_coverage_flags: summary.partialCoverage,
default_only_flags: summary.defaultOnlyCoverage,
coverage_percentage: summary.coveragePercentage,
};
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
Expand All @@ -994,6 +1023,7 @@ async function main() {
{ namespace: 'e2e', collect: collectE2ECounts },
{ namespace: 'metametrics', collect: collectMetametricsQaStats },
{ namespace: 'performance', collect: collectPerformanceTestCounts },
{ namespace: 'feature_flags', collect: collectFeatureFlagCoverage },
];

for (const { namespace, collect } of collectors) {
Expand Down
14 changes: 6 additions & 8 deletions .github/workflows/build-android-upload-to-browserstack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,12 @@ jobs:
exit 1
fi

# Set environment variables for without-SRP build
ENV_VARS='[
{
"mapped_to": "DISABLE_NOTIFICATION_PROMPT",
"value": "true",
"is_expand": true
}
]'
# Without-SRP build: seedless / BrowserStack perf onboarding (E2E_MOCK_OAUTH).
# Optional E2E_MOCK_OAUTH_EMAIL overrides request email_id (default newuser+e2e@web3auth.io).
ENV_VARS=$(jq -n \
--arg byoa "${{ secrets.E2E_BYOA_AUTH_SECRET }}" \
--arg email "${{ secrets.E2E_MOCK_OAUTH_EMAIL }}" \
'[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true}]')
CUSTOM_ID="MetaMask-Android-Without-SRP-${{ github.run_id }}"

# Trigger Android workflow
Expand Down
14 changes: 6 additions & 8 deletions .github/workflows/build-ios-upload-to-browserstack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,12 @@ jobs:
exit 1
fi

# Set environment variables for without-SRP build
ENV_VARS='[
{
"mapped_to": "DISABLE_NOTIFICATION_PROMPT",
"value": "true",
"is_expand": true
}
]'
# Without-SRP build: seedless / BrowserStack perf onboarding (E2E_MOCK_OAUTH).
# Optional E2E_MOCK_OAUTH_EMAIL overrides request email_id (default newuser+e2e@web3auth.io).
ENV_VARS=$(jq -n \
--arg byoa "${{ secrets.E2E_BYOA_AUTH_SECRET }}" \
--arg email "${{ secrets.E2E_MOCK_OAUTH_EMAIL }}" \
'[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true}]')
CUSTOM_ID="MetaMask-iOS-Without-SRP-${{ github.run_id }}"

# Trigger iOS workflow
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ jobs:
NODE_OPTIONS: --max_old_space_size=12288

- name: Check bundle size
run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 53
run: ./scripts/js-bundle-stats.sh ios/main.jsbundle 54

- name: Upload iOS bundle
uses: actions/upload-artifact@v4
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/qa-stats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: yarn

- name: Install dependencies
run: yarn install --immutable

- name: Collect QA stats
run: node .github/scripts/collect-qa-stats.mjs
Expand All @@ -28,3 +32,10 @@ jobs:
name: qa-stats
path: ./qa-stats.json
if-no-files-found: error

- name: Upload feature flag coverage report
uses: actions/upload-artifact@v6
with:
name: feature-flag-coverage-report
path: ./tests/artifacts/feature-flag-coverage-report.json
if-no-files-found: warn
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# typescript incremental build cache
.tsbuildinfo
*.tsbuildinfo

# task working directory (agent-local, not shipped)
.task/
Expand Down
14 changes: 14 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export SENTRY_DISABLE_AUTO_UPLOAD="true"
# ENV vars for e2e tests
# Only enable it for e2e tests
export IS_TEST="false"
# Performance E2E (Appwright): set on the bundle step in CI so seedless/OAuth Metro mocks are explicitly opted in.
# Optional locally when pairing with METAMASK_ENVIRONMENT=e2e.
# export PERFORMANCE_TEST_JOB="true"
# Force-disable seedless + OAuth Metro redirects while keeping other E2E behavior (Sentry mocks, etc.):
# export E2E_USE_SEEDLESS_OAUTH_METRO_MOCK="false"
# Finer control: disable OAuth handler Metro mock for non-seedless
# onboarding perf builds; keep seedless perf on default (unset) so seedless-*.spec.js use mocks.
# export E2E_USE_OAUTH_LOGIN_HANDLERS_METRO_MOCK="false"
# export E2E_USE_SEEDLESS_CONTROLLER_METRO_MOCK="false"
# defined as secrets to run on Bitrise CI
# but have to be defined here for local tests
export MM_TEST_ACCOUNT_SRP=""
Expand Down Expand Up @@ -147,6 +156,11 @@ export RAMP_INTERNAL_BUILD="true"
# To enable seedless onboarding ( set to true for seedless onboarding )
export SEEDLESS_ONBOARDING_ENABLED='false'

# Mock OAuth for performance tests on BrowserStack
export E2E_MOCK_OAUTH='false'
export E2E_BYOA_AUTH_SECRET=''
export E2E_MOCK_OAUTH_EMAIL=''

# env for seedless onboarding main-dev
export ANDROID_APPLE_CLIENT_ID='io.metamask.appleloginclient.dev'
export ANDROID_GOOGLE_CLIENT_ID='8615965109465-i8oeh9kuvl1n6lk1ffkobpvth27bmi41.apps.googleusercontent.com'
Expand Down
38 changes: 38 additions & 0 deletions app/components/UI/Bridge/Views/BridgeView/BridgeView.view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,44 @@ describeForPlatforms('BridgeView', () => {
expect(await findByText('dest')).toBeOnTheScreen();
});

describe('Gasless swap', () => {
it('shows error banner when gasless swap quote fetch fails and dismisses it on close', async () => {
const now = Date.now();

const { findByText, queryByText, getByTestId } = defaultBridgeWithTokens({
engine: {
backgroundState: {
BridgeController: {
quotes: [],
recommendedQuote: null,
quotesLastFetched: now,
quotesLoadingStatus: RequestStatus.FETCHED,
quoteFetchError: 'GaslessSwapSubmissionFailed',
},
RemoteFeatureFlagController: {
remoteFeatureFlags: {
gasFeesSponsoredNetwork: { '0x1': true },
},
},
},
},
} as unknown as Record<string, unknown>);

// Error banner appears after the gasless swap quote fetch failure
expect(
await findByText(strings('bridge.error_banner_description')),
).toBeOnTheScreen();

// User dismisses the error banner
fireEvent.press(getByTestId(CommonSelectorsIDs.BANNER_CLOSE_BUTTON_ICON));

// Banner is gone after dismissal
expect(
queryByText(strings('bridge.error_banner_description')),
).not.toBeOnTheScreen();
});
});

describe('Swap team regression (bug matrix team-swaps-and-bridge)', () => {
/** Issues covered: #24744, #24865, #24802, #25256 */
// eslint-disable-next-line @metamask/design-tokens/color-no-hex -- "#24744" style references are GitHub issue IDs (e.g. "#2342"), not color literals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,21 @@ const MarketInsightsView: React.FC = () => {
return report.trends.flatMap((trend) => trend.tweets).slice(0, 4);
}, [report]);
const handleBackPress = useCallback(() => {
const event = createEventBuilder(MetaMetricsEvents.MARKET_INSIGHTS_CLOSED)
.addProperties({
...assetIdProperty,
...assetSymbolProperty,
})
.build();
trackEvent(event);
navigation.goBack();
}, [navigation]);
}, [
navigation,
trackEvent,
createEventBuilder,
assetIdProperty,
assetSymbolProperty,
]);

const handleTweetPress = useCallback((url: string) => {
if (isSafeUrl(url)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,21 @@ describe('SendTransaction View', () => {
};
});

it('does not crash when order data has no cryptoCurrency', async () => {
const orderWithoutCrypto = {
...mockOrder,
id: 'test-id-no-crypto',
data: {
...mockOrder.data,
cryptoCurrency: undefined,
} as DeepPartial<SellOrder>,
} as FiatOrder;

mockUseParamsValues = { orderId: 'test-id-no-crypto' };
render(SendTransaction, [orderWithoutCrypto]);
expect(screen.queryByText('Next')).not.toBeOnTheScreen();
});

it('calls setOptions when rendering', async () => {
render(SendTransaction);
expect(mockSetOptions).toBeCalledTimes(1);
Expand Down Expand Up @@ -529,4 +544,60 @@ describe('SendTransaction View', () => {
]
`);
});

describe('transactionAnalyticsPayload with partial order data', () => {
it('handles missing cryptoCurrency gracefully in analytics', async () => {
const partialOrder = {
...mockOrder,
id: 'test-partial-crypto',
data: {
...mockOrder.data,
cryptoCurrency: undefined,
} as DeepPartial<SellOrder>,
} as FiatOrder;

mockUseParamsValues = { orderId: 'test-partial-crypto' };
render(SendTransaction, [partialOrder]);
expect(mockTrackEvent).toHaveBeenCalledWith(
'OFFRAMP_SEND_CRYPTO_PROMPT_VIEWED',
expect.objectContaining({
chain_id_source: undefined,
currency_source: undefined,
crypto_amount: '0.012361263',
order_id: 'test-partial-crypto',
}),
);
});

it('handles missing cryptoCurrency.network gracefully in analytics and does not invoke send', async () => {
const partialOrder = {
...mockOrder,
id: 'test-partial-network',
data: {
...mockOrder.data,
cryptoCurrency: {
...(mockOrder.data as DeepPartial<SellOrder>).cryptoCurrency,
network: undefined,
},
} as DeepPartial<SellOrder>,
} as FiatOrder;

mockUseParamsValues = { orderId: 'test-partial-network' };
render(SendTransaction, [partialOrder]);
expect(mockTrackEvent).toHaveBeenCalledWith(
'OFFRAMP_SEND_CRYPTO_PROMPT_VIEWED',
expect.objectContaining({
chain_id_source: undefined,
currency_source: 'ETH',
currency_destination: 'USD',
payment_method_id: '/payments/instant-bank-transfer',
provider_offramp: 'Test (Staging)',
}),
);

const nextButton = screen.getByRole('button', { name: 'Next' });
await act(async () => fireEvent.press(nextButton));
expect(mockAddTransaction).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ function SendTransaction() {
const transactionAnalyticsPayload = useMemo(
() => ({
crypto_amount: orderData?.cryptoAmount as string,
chain_id_source: orderData?.cryptoCurrency.network.chainId,
chain_id_source: orderData?.cryptoCurrency?.network?.chainId,
fiat_out: orderData?.fiatAmount,
payment_method_id: orderData?.paymentMethod.id,
currency_source: orderData?.cryptoCurrency.symbol,
currency_destination: orderData?.fiatCurrency.symbol,
payment_method_id: orderData?.paymentMethod?.id,
currency_source: orderData?.cryptoCurrency?.symbol,
currency_destination: orderData?.fiatCurrency?.symbol,
order_id: order?.id,
provider_offramp: orderData?.provider.name,
provider_offramp: orderData?.provider?.name,
}),
[order?.id, orderData],
);
Expand All @@ -140,9 +140,12 @@ function SendTransaction() {
}, [trackEvent, transactionAnalyticsPayload]);

const handleSend = useCallback(async () => {
const chainId = orderData?.cryptoCurrency?.network?.chainId;
if (!chainId) return;

let chainIdAsHex: `0x${string}`;
try {
chainIdAsHex = toHex(orderData.cryptoCurrency.network.chainId);
chainIdAsHex = toHex(chainId);
} catch {
return;
}
Expand Down Expand Up @@ -229,7 +232,7 @@ function SendTransaction() {
networkClientId,
]);

if (!order) {
if (!order || !orderData?.cryptoCurrency) {
return null;
}

Expand Down
Loading
Loading