Skip to content

Commit b6f3016

Browse files
authored
Merge branch 'main' into fix/qr-connect-view
2 parents 6dece38 + df201b1 commit b6f3016

77 files changed

Lines changed: 6473 additions & 3998 deletions

File tree

Some content is hidden

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

CHANGELOG.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [7.76.0]
11+
12+
### Added
13+
14+
- Added the Tempo chain to the additional networks list. (#29515)
15+
- Added security trust badges in the token list. (#29509)
16+
- Added a new "Add money" bottom sheet to the Money Account homepage with Convert crypto, Deposit funds, Move mUSD, and Receive from external wallet options. (#29336)
17+
- Added a developer options button to clear dismissed mUSD conversion asset details CTAs. (#29510)
18+
- Added a quickbuy smart default. (#29472)
19+
- Added SOL as a "Pay with" option on Quickbuy. (#29424)
20+
- Added a transaction fees expandable section to Quickbuy. (#29321)
21+
- Added a General Settings toggle for haptic feedback and centralized in-app haptics behind a single module, with an optional remote kill switch. (#28975)
22+
- Added haptic feedback across Top Traders surfaces (Follow, Buy, notification preferences, per-trader notifications). (#29467)
23+
- Added support for additional sports market types (spreads, totals, halftime result, exact score, player props) on prediction market event detail screens. (#29216)
24+
- Added inline error banners in the Predict buy bottom sheet for price-changed and order-failed scenarios, replacing the secondary retry sheet and failure toast when the predictBottomSheet flag is enabled. (#29184)
25+
- Added a Quickbuy half sheet. (#28863)
26+
- Added a warning flow in bridge quotes when token price data is unavailable. (#29250)
27+
1028
### Changed
1129

12-
- Aligned previously base-enabled custom network logos (Stable, Flow, XDC, Fraxtal, Hemi, Plasma, Lukso, Rootstock, MSU, Lens, Plume) to a square format consistent with Popular networks (#29943)
30+
- Enabled Suspicious and Malicious security badges on tokens in the Swaps/Bridge asset picker. (#29570)
31+
- Updated the description under the "Smart account requests from dapps" setting to clarify that MetaMask will only upgrade to our audited smart account. (#29211)
32+
- Updated Predict confirmations to display pUSD as the Predict token. (#29450)
33+
- Updated the token details security trust designs. (#29230)
34+
- Polished the social leaderboard with podium decorations on the top 3 traders, a tighter loading skeleton, and a chain icon overlay on token avatars in the trader position and quick buy views. (#29408)
35+
- QR wallet now shares the signing flow with Ledger. (#29087)
36+
- In-app browser sessions opened from MetaMask Card (manage card and travel) now return to Card Home on close instead of the Explore tab. (#29218)
37+
- Updated the Bridge/Swaps token warning modal to match the new design and fixed swapped warning/malicious icons in the token selector. (#29197)
38+
- Updated the account selector to use the full-page list for all users and removed the dropdown arrow from the wallet account picker. (#28701)
39+
- Refined section and header styles for Perps. (#29009)
40+
- Refined explore page design patterns. (#29396)
41+
- Adjusted spacing for money activity filters. (#29463)
42+
43+
### Fixed
44+
45+
- Fixed a bug where the -1%/-2% limit price preset buttons only applied on the first press. (#29373)
46+
- Fixed Card Home flickering during refresh and kept Add funds available for frozen cards. (#29513)
47+
- Fixed a bug that caused the MetaMask Card spending limit screen to default to full access when a custom limit was already set. (#29517)
48+
- Fixed cashback redemption to require an approved Linea funding source. (#29489)
49+
- Fixed UB2 Transak order details to hide interim provider messages during processing states. (#29131)
50+
- Fixed a bug where a stale secondary minimum purchase message could briefly appear after entering a valid buy amount. (#29365)
51+
- Fixed a bug that caused live Predict sports scores to appear in reverse order for some leagues. (#29453)
52+
- Fixed Ledger BLE transport that prevented reconnecting to a Ledger device after a timeout or completed signing flow. (#29367)
53+
- Fixed chart re-rendering and flickering. (#29344)
54+
- Fixed excessive red flashing while entering buy amounts near minimum and maximum provider limits. (#29360)
55+
- Fixed stale unauthenticated MetaMask Card data after login by refetching when card feature flags change. (#29350)
56+
- Prevented invalid bridge transaction hashes from being persisted in transaction history. (#29136)
57+
- Fixed WalletConnect sign requests occasionally displaying a garbled string in the "Request from" field instead of the dapp's domain. (#29102)
58+
- Fixed Perps Withdraw Max so users actually withdraw their full HyperLiquid balance; replaced 90% with a Max button and aligned Perps home and Withdraw to show the same balance. (#29257)
59+
- Hid the sponsored label on cross-chain bridge with insufficient balance. (#29490)
1360

1461
## [7.75.1]
1562

@@ -11375,7 +11422,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1137511422
- [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957)
1137611423
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
1137711424

11378-
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.1...HEAD
11425+
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...HEAD
11426+
[7.76.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.1...v7.76.0
1137911427
[7.75.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.0...v7.75.1
1138011428
[7.75.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.74.3...v7.75.0
1138111429
[7.74.3]: https://github.com/MetaMask/metamask-mobile/compare/v7.74.2...v7.74.3

app/components/UI/Perps/adapters/mobileInfrastructure.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,14 @@ describe('createMobileInfrastructure', () => {
215215
const caipAccountId =
216216
'eip155:42161:0x1234' as `${string}:${string}:${string}`;
217217

218-
const result =
219-
await infra.rewards.getPerpsDiscountForAccount(caipAccountId);
218+
const result = await infra.rewards.getPerpsDiscountForAccount(
219+
caipAccountId,
220+
10,
221+
);
220222

221223
expect(
222224
Engine.context.RewardsController.getPerpsDiscountForAccount,
223-
).toHaveBeenCalledWith(caipAccountId);
225+
).toHaveBeenCalledWith(caipAccountId, 10);
224226
expect(result).toBe(5);
225227
});
226228
});

app/components/UI/Perps/adapters/mobileInfrastructure.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,11 @@ export function createMobileInfrastructure(): PerpsPlatformDependencies {
302302
rewards: {
303303
getPerpsDiscountForAccount(
304304
caipAccountId: `${string}:${string}:${string}`,
305+
baseFeeBips: number,
305306
) {
306307
return Engine.context.RewardsController.getPerpsDiscountForAccount(
307308
caipAccountId,
309+
baseFeeBips,
308310
);
309311
},
310312
},

app/components/UI/Perps/hooks/usePerpsCloseAllCalculations.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,47 @@ describe('usePerpsCloseAllCalculations', () => {
490490
expect(result.current.avgFeeDiscountPercentage).toBe(65);
491491
});
492492

493+
it('does not apply a discount and allows retry when controller returns null (unhydrated)', async () => {
494+
// Arrange: subscription state not hydrated yet
495+
mockGetPerpsDiscount.mockResolvedValue(null);
496+
497+
const positions = [createMockPosition({ symbol: 'BTC' })];
498+
const priceData = { BTC: { price: '51000' } };
499+
500+
mockCalculateFees.mockResolvedValue(
501+
createMockFeeResult({
502+
feeAmount: 275,
503+
metamaskFeeRate: 0.01,
504+
metamaskFeeAmount: 250,
505+
protocolFeeRate: 0.001,
506+
protocolFeeAmount: 25,
507+
}),
508+
);
509+
510+
// Act
511+
const { result, rerender } = renderHook(
512+
({ pos }: { pos: Position[] }) =>
513+
usePerpsCloseAllCalculations({ positions: pos, priceData }),
514+
{ initialProps: { pos: positions } },
515+
);
516+
517+
await waitFor(() => expect(result.current.isLoading).toBe(false));
518+
519+
// No discount applied; original rate matches base rate
520+
expect(result.current.avgFeeDiscountPercentage).toBeUndefined();
521+
expect(result.current.avgMetamaskFeeRate).toBeCloseTo(0.01, 4);
522+
expect(result.current.totalFees).toBe(275);
523+
524+
// Hydration completes and a positions change retries the fetch
525+
mockGetPerpsDiscount.mockResolvedValueOnce(6500);
526+
rerender({ pos: [...positions] });
527+
528+
await waitFor(() =>
529+
expect(result.current.avgFeeDiscountPercentage).toBe(65),
530+
);
531+
expect(mockGetPerpsDiscount).toHaveBeenCalledTimes(2);
532+
});
533+
493534
it('handles discount fetch errors gracefully', async () => {
494535
// Arrange: Discount API fails
495536
mockGetPerpsDiscount.mockRejectedValue(new Error('API error'));

app/components/UI/Perps/hooks/usePerpsCloseAllCalculations.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useMemo, useState, useEffect, useRef } from 'react';
22
import { useSelector } from 'react-redux';
33
import {
4+
BASIS_POINTS_DIVISOR,
5+
BUILDER_FEE_CONFIG,
46
formatAccountToCaipAccountId,
57
type Position,
68
type FeeCalculationResult,
@@ -12,6 +14,7 @@ import type {
1214
import Engine from '../../../../core/Engine';
1315
import { selectSelectedAccountGroupEvmInternalAccount } from '../../../../selectors/multichainAccounts/accountTreeController';
1416
import { selectChainId } from '../../../../selectors/networkController';
17+
import { DevLogger } from '../../../../core/SDKConnect/utils/DevLogger';
1518

1619
/**
1720
* Aggregated calculations result for closing all positions
@@ -105,6 +108,9 @@ export function usePerpsCloseAllCalculations({
105108
const isComponentMountedRef = useRef(true);
106109
const discountFetchCounterRef = useRef(0);
107110
const calculationCounterRef = useRef(0);
111+
// Tracks the account the freeze was set for, so position changes don't
112+
// spuriously reset it but account switches do.
113+
const discountAccountKeyRef = useRef<string | undefined>(undefined);
108114

109115
// State for per-position calculations
110116
const [perPositionResults, setPerPositionResults] = useState<
@@ -143,12 +149,19 @@ export function usePerpsCloseAllCalculations({
143149
);
144150

145151
// Fetch account-level fee discount (applies uniformly to all positions)
146-
// Freeze mechanism prevents refetching when only positions change
152+
// Freeze mechanism prevents refetching once we have a hydrated answer.
153+
// Positions are included in deps so an unhydrated result (null) gets a
154+
// retry on the next positions change instead of locking in no-discount.
147155
useEffect(() => {
148156
// Increment counter to invalidate any in-flight requests
149157
const currentFetchId = ++discountFetchCounterRef.current;
150-
// Reset freeze when account changes (allow refetch for new account)
151-
hasValidDiscountRef.current = false;
158+
// Only reset freeze when the account actually changes — keep it across
159+
// positions changes so a successful fetch isn't re-run on every tick.
160+
const accountKey = `${selectedAddress ?? ''}-${currentChainId ?? ''}`;
161+
if (discountAccountKeyRef.current !== accountKey) {
162+
hasValidDiscountRef.current = false;
163+
discountAccountKeyRef.current = accountKey;
164+
}
152165

153166
async function fetchFeeDiscount() {
154167
// Skip if already have valid discount for this account (freeze guard)
@@ -188,15 +201,28 @@ export function usePerpsCloseAllCalculations({
188201
const discountBips =
189202
await Engine.context.RewardsController.getPerpsDiscountForAccount(
190203
caipAccountId,
204+
BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR,
191205
);
192206

193207
// Only update state if this is still the latest fetch and component is mounted
194208
if (
195209
discountFetchCounterRef.current === currentFetchId &&
196210
isComponentMountedRef.current
197211
) {
198-
setFeeDiscountBips(discountBips);
199-
hasValidDiscountRef.current = true;
212+
if (discountBips === null) {
213+
// Subscription state hasn't hydrated yet — don't cache the
214+
// no-discount value. Freeze stays off so the next positions
215+
// change retries the fetch.
216+
DevLogger.log(
217+
'Rewards: fee discount unhydrated for close-all flow, will retry on next positions change',
218+
{ selectedAddress, currentChainId },
219+
);
220+
setFeeDiscountBips(0);
221+
hasValidDiscountRef.current = false;
222+
} else {
223+
setFeeDiscountBips(discountBips);
224+
hasValidDiscountRef.current = true;
225+
}
200226
}
201227
} catch (error) {
202228
console.warn('Failed to fetch fee discount:', error);
@@ -214,7 +240,7 @@ export function usePerpsCloseAllCalculations({
214240
fetchFeeDiscount().catch((error) => {
215241
console.error('Unhandled error in fetchFeeDiscount:', error);
216242
});
217-
}, [selectedAddress, currentChainId]);
243+
}, [selectedAddress, currentChainId, positions]);
218244

219245
// Per-position fee and rewards calculation
220246
// This ensures accurate coin-specific rewards calculation

app/components/UI/Perps/hooks/usePerpsOrderFees.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,38 @@ describe('usePerpsOrderFees', () => {
497497
expect(result.current.originalMetamaskFeeRate).toBe(0.01);
498498
// The hook should apply discount internally
499499
});
500+
501+
it('does not apply a discount when controller returns null discountBips', async () => {
502+
mockEngineContext.RewardsController.getPerpsDiscountForAccount.mockResolvedValueOnce(
503+
null,
504+
);
505+
506+
const mockFeeResult: FeeCalculationResult = {
507+
feeRate: 0.01045,
508+
feeAmount: 1045,
509+
protocolFeeRate: 0.00045,
510+
metamaskFeeRate: 0.01,
511+
};
512+
mockCalculateFees.mockResolvedValue(mockFeeResult);
513+
514+
const { result } = renderHook(
515+
() =>
516+
usePerpsOrderFees({
517+
orderType: 'market',
518+
amount: '100000',
519+
}),
520+
{ wrapper: createWrapper() },
521+
);
522+
523+
await waitFor(() => {
524+
expect(result.current.isLoadingMetamaskFee).toBe(false);
525+
});
526+
527+
expect(result.current.feeDiscountPercentage).toBeUndefined();
528+
expect(result.current.metamaskFeeRate).toBe(0.01);
529+
expect(result.current.originalMetamaskFeeRate).toBe(0.01);
530+
expect(result.current.metamaskFee).toBe(1000); // 100000 * 0.01, undiscounted
531+
});
500532
});
501533

502534
describe('Loading states', () => {

app/components/UI/Perps/hooks/usePerpsOrderFees.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
EstimatedPointsDto,
1313
} from '../../../../core/Engine/controllers/rewards-controller/types';
1414
import {
15+
BASIS_POINTS_DIVISOR,
16+
BUILDER_FEE_CONFIG,
1517
PerpsMeasurementName,
1618
PERFORMANCE_CONFIG,
1719
formatAccountToCaipAccountId,
@@ -182,8 +184,17 @@ export function usePerpsOrderFees({
182184

183185
const { RewardsController } = Engine.context;
184186
const feeDiscountStartTime = performance.now();
185-
const discountBips =
186-
await RewardsController.getPerpsDiscountForAccount(caipAccountId);
187+
const discountBips = await RewardsController.getPerpsDiscountForAccount(
188+
caipAccountId,
189+
BUILDER_FEE_CONFIG.MaxFeeDecimal * BASIS_POINTS_DIVISOR,
190+
);
191+
if (discountBips === null) {
192+
DevLogger.log('Rewards: No fee discount available', {
193+
address,
194+
caipAccountId,
195+
});
196+
return { discountBips: undefined };
197+
}
187198
const feeDiscountDuration = performance.now() - feeDiscountStartTime;
188199

189200
// Measure fee discount API call performance

app/components/Views/Homepage/Sections/DeFi/DeFiSection.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ const DeFiSection = forwardRef<SectionRefreshHandle, DeFiSectionProps>(
126126

127127
useSectionPerformance({
128128
sectionId: HomeSectionNames.DEFI,
129-
// Align with other sections: loading finished without error = ready (empty is success + content_state empty).
130-
contentReady: !isLoading && !hasError,
131-
isEmpty: isEmpty || hasError,
129+
contentReady: !isLoading,
130+
isEmpty: isEmpty && !hasError,
131+
contentStateForTrace: hasError ? 'error' : undefined,
132132
isLoading,
133133
enabled: isDeFiEnabled,
134134
});

app/components/Views/Homepage/Sections/Perpetuals/PerpsSection.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,10 @@ const PerpsSectionMain = forwardRef<SectionRefreshHandle, PerpsSectionProps>(
412412
});
413413

414414
useSectionPerformance({
415-
sectionId: HomeSectionNames.PERPS,
416-
contentReady: !isLoadingSection && !connectionError,
415+
sectionId: analyticsName,
416+
contentReady: !isLoadingSection,
417417
isEmpty: !hasItems,
418+
contentStateForTrace: connectionError ? 'error' : undefined,
418419
isLoading: isLoadingSection,
419420
});
420421

app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,25 @@ const PredictionsSectionDefault = forwardRef<
454454
(positionsError || marketsError);
455455
const isEmpty =
456456
!isLoading && !hasAnyPositions && markets.length === 0 && !hasError;
457-
const willRender = isPredictEnabled && !isLoading && !isEmpty && !hasError;
457+
const showTrendingAbove =
458+
!hasPositions &&
459+
!isLoadingPositions &&
460+
(isLoadingMarkets || markets.length > 0);
461+
const inPositionsLayout =
462+
hasAnyPositions || isLoadingPositions || isLoadingClaimable;
463+
/** TTC: no Predict position-row or market-card skeleton; claimable / other fetch may continue. */
464+
const predictTimeToContentReady = Boolean(
465+
isPredictEnabled &&
466+
(hasError ||
467+
(inPositionsLayout
468+
? !isLoadingPositions && (hasPositions || !isLoadingMarkets)
469+
: !isLoadingMarkets)),
470+
);
471+
const willRender =
472+
isPredictEnabled &&
473+
!hasError &&
474+
!isLoading &&
475+
(hasAnyPositions || markets.length > 0);
458476
const itemCount = hasPositions
459477
? positions.length
460478
: hasClaimablePositions
@@ -472,9 +490,10 @@ const PredictionsSectionDefault = forwardRef<
472490
});
473491

474492
useSectionPerformance({
475-
sectionId: HomeSectionNames.PREDICT,
476-
contentReady: willRender,
477-
isEmpty: isEmpty || !!hasError,
493+
sectionId: analyticsName,
494+
contentReady: predictTimeToContentReady,
495+
isEmpty: isEmpty && !hasError,
496+
contentStateForTrace: hasError ? 'error' : undefined,
478497
isLoading,
479498
enabled: isPredictEnabled,
480499
});
@@ -495,11 +514,6 @@ const PredictionsSectionDefault = forwardRef<
495514
}
496515

497516
if (hasAnyPositions || isLoadingPositions || isLoadingClaimable) {
498-
const showTrendingAbove =
499-
!hasPositions &&
500-
!isLoadingPositions &&
501-
(isLoadingMarkets || markets.length > 0);
502-
503517
return (
504518
<View ref={sectionViewRef} onLayout={onLayout}>
505519
{showTrendingAbove && (

0 commit comments

Comments
 (0)