Skip to content

Commit 2c82210

Browse files
authored
Release/7.81.2 (OTA) (#31926)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- 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: ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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.
2 parents b95589a + f5e5f6d commit 2c82210

57 files changed

Lines changed: 3708 additions & 2011 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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [7.81.2]
11+
12+
### Changed
13+
14+
- Enhance PredictMarketRowItem with endAccessory prop and remove NBA champion discovery references. (#31923)
15+
16+
### Fixed
17+
18+
- Fix odds prices stale on feed cards and event page during live markets (#31922)
19+
- Fix extended sports outcome grouping, spread ordering, and live pricing (#31934)
20+
1021
## [7.81.1]
1122

1223
### Added
1324

1425
- Adds Swap/Bridge feature to ARC mainnet. (#31413)
1526

27+
### Changed
28+
29+
- chore: Enhance PredictMarketRowItem with endAccessory prop and remove NBA champion discovery references (#31923)
30+
1631
### Fixed
1732

1833
- Fixed the MetaMask Card section on the Money screen so the "Link card" option and funding token placeholders are only shown when the card spending token is enabled in the card feature configuration. (#31611)
@@ -12087,7 +12102,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1208712102
- [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957)
1208812103
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
1208912104

12090-
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.81.1...HEAD
12105+
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.81.2...HEAD
12106+
[7.81.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.81.1...v7.81.2
1209112107
[7.81.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.81.0...v7.81.1
1209212108
[7.81.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.80.0...v7.81.0
1209312109
[7.80.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.79.1...v7.80.0

app/components/UI/Predict/components/FeaturedCarousel/FeaturedCarouselSportCard.test.tsx

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../../types';
1414
import FeaturedCarouselSportCard from './FeaturedCarouselSportCard';
1515
import { FEATURED_CAROUSEL_TEST_IDS } from './FeaturedCarousel.testIds';
16+
import { useLiveMarketPrices } from '../../hooks/useLiveMarketPrices';
1617

1718
jest.mock('@metamask/design-system-twrnc-preset', () => ({
1819
useTailwind: () => ({
@@ -83,6 +84,14 @@ jest.mock('../../hooks/useLiveGameUpdates', () => ({
8384
useLiveGameUpdates: () => ({ gameUpdate: null }),
8485
}));
8586

87+
const mockGetLivePrice = jest.fn();
88+
jest.mock('../../hooks/useLiveMarketPrices', () => ({
89+
useLiveMarketPrices: jest.fn(() => ({
90+
getPrice: mockGetLivePrice,
91+
})),
92+
}));
93+
const mockUseLiveMarketPrices = jest.mocked(useLiveMarketPrices);
94+
8695
jest.mock('../../constants/sportLeagueConfigs', () => ({
8796
getLeagueConfig: () => ({}),
8897
}));
@@ -107,6 +116,24 @@ const initialState = {
107116
},
108117
};
109118

119+
const stateWithSportCardLivePricesEnabled = (enabled: boolean) => ({
120+
engine: {
121+
backgroundState: {
122+
...backgroundState,
123+
RemoteFeatureFlagController: {
124+
...backgroundState.RemoteFeatureFlagController,
125+
remoteFeatureFlags: {
126+
...backgroundState.RemoteFeatureFlagController?.remoteFeatureFlags,
127+
predictSportCardLivePrices: {
128+
enabled,
129+
minimumVersion: '0.0.0',
130+
},
131+
},
132+
},
133+
},
134+
},
135+
});
136+
110137
const createMockOutcome = (
111138
tokens: PredictOutcomeToken[],
112139
volume = 1500000,
@@ -184,6 +211,7 @@ const createMockSportMarket = (
184211
describe('FeaturedCarouselSportCard', () => {
185212
beforeEach(() => {
186213
jest.clearAllMocks();
214+
mockGetLivePrice.mockReturnValue(undefined);
187215
});
188216

189217
it('renders league name and live indicator for ongoing games', () => {
@@ -257,7 +285,9 @@ describe('FeaturedCarouselSportCard', () => {
257285
{ state: initialState },
258286
);
259287

260-
expect(getByText(strings('predict.outcome_draw'))).toBeOnTheScreen();
288+
expect(
289+
getByText(`${strings('predict.outcome_draw')} 20%`),
290+
).toBeOnTheScreen();
261291
});
262292

263293
it.each(['nba', 'nfl'] as const)(
@@ -414,4 +444,60 @@ describe('FeaturedCarouselSportCard', () => {
414444
}),
415445
);
416446
});
447+
448+
it('renders live best ask prices when available', () => {
449+
mockGetLivePrice.mockImplementation((tokenId: string) => ({
450+
tokenId,
451+
price: 0,
452+
bestBid: 0,
453+
bestAsk:
454+
tokenId === 'home-token'
455+
? 0.75
456+
: tokenId === 'draw-token'
457+
? 0.18
458+
: 0.25,
459+
}));
460+
const market = createMockSportMarket();
461+
462+
const { getByText } = renderWithProvider(
463+
<FeaturedCarouselSportCard market={market} index={0} />,
464+
{ state: initialState },
465+
);
466+
467+
expect(getByText('$133.33')).toBeOnTheScreen();
468+
expect(getByText('$400.00')).toBeOnTheScreen();
469+
expect(getByText('75%')).toBeOnTheScreen();
470+
expect(
471+
getByText(`${strings('predict.outcome_draw')} 18%`),
472+
).toBeOnTheScreen();
473+
expect(getByText('25%')).toBeOnTheScreen();
474+
});
475+
476+
it('renders static prices and disables live subscriptions when the flag is off', () => {
477+
mockGetLivePrice.mockImplementation((tokenId: string) => ({
478+
tokenId,
479+
price: 0,
480+
bestBid: 0,
481+
bestAsk: 0.99,
482+
}));
483+
const market = createMockSportMarket();
484+
485+
const { getByText, queryByText } = renderWithProvider(
486+
<FeaturedCarouselSportCard market={market} index={0} />,
487+
{ state: stateWithSportCardLivePricesEnabled(false) },
488+
);
489+
490+
expect(getByText('$166.67')).toBeOnTheScreen();
491+
expect(getByText('$250.00')).toBeOnTheScreen();
492+
expect(getByText('60%')).toBeOnTheScreen();
493+
expect(
494+
getByText(`${strings('predict.outcome_draw')} 20%`),
495+
).toBeOnTheScreen();
496+
expect(getByText('40%')).toBeOnTheScreen();
497+
expect(queryByText('99%')).not.toBeOnTheScreen();
498+
expect(mockUseLiveMarketPrices).toHaveBeenLastCalledWith(
499+
['home-token', 'draw-token', 'away-token'],
500+
{ enabled: false },
501+
);
502+
});
417503
});

app/components/UI/Predict/components/FeaturedCarousel/FeaturedCarouselSportCard.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useMemo } from 'react';
22
import { TouchableOpacity } from 'react-native';
33
import { NavigationProp, useNavigation } from '@react-navigation/native';
4+
import { useSelector } from 'react-redux';
45
import { useTailwind } from '@metamask/design-system-twrnc-preset';
56
import {
67
Box,
@@ -19,6 +20,7 @@ import Routes from '../../../../../constants/navigation/Routes';
1920
import {
2021
PredictMarket,
2122
PredictMarketGame,
23+
PredictMarketStatus,
2224
PredictOutcomeToken,
2325
} from '../../types';
2426
import {
@@ -30,10 +32,13 @@ import { PredictEventValues } from '../../constants/eventNames';
3032
import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
3133
import { usePredictPreviewSheet } from '../../contexts';
3234
import { useLiveGameUpdates } from '../../hooks/useLiveGameUpdates';
35+
import { useLiveMarketPrices } from '../../hooks/useLiveMarketPrices';
3336
import { isDrawCapableLeague } from '../../constants/sports';
37+
import { selectPredictSportCardLivePricesEnabledFlag } from '../../selectors/featureFlags';
3438
import PredictSportTeamLogo from '../PredictSportTeamLogo/PredictSportTeamLogo';
3539
import { getLeagueConfig } from '../../constants/sportLeagueConfigs';
3640
import { parseScore } from '../../utils/gameParser';
41+
import { isValidPrice } from '../../utils/prices';
3742
import FeaturedCarouselCardFooter from './FeaturedCarouselCardFooter';
3843
import FeaturedCarouselPayoutRow from './FeaturedCarouselPayoutRow';
3944
import { FEATURED_CAROUSEL_TEST_IDS } from './FeaturedCarousel.testIds';
@@ -62,6 +67,9 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
6267
useNavigation<NavigationProp<PredictNavigationParamList>>();
6368
const { openBuySheet } = usePredictPreviewSheet();
6469
const { executeGuardedAction } = usePredictActionGuard({ navigation });
70+
const livePricesEnabled = useSelector(
71+
selectPredictSportCardLivePricesEnabledFlag,
72+
);
6573

6674
const game = market.game as PredictMarketGame;
6775
const config = getLeagueConfig(game.league);
@@ -117,6 +125,31 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
117125
const drawToken = showDraw
118126
? outcome?.tokens?.find((t) => t.title?.toLowerCase() === 'draw')
119127
: undefined;
128+
const tokenIds = useMemo(
129+
() =>
130+
[homeToken, drawToken, awayToken]
131+
.map((token) => token?.id)
132+
.filter((id): id is string => Boolean(id)),
133+
[awayToken, drawToken, homeToken],
134+
);
135+
const { getPrice } = useLiveMarketPrices(tokenIds, {
136+
enabled:
137+
livePricesEnabled &&
138+
market.status === PredictMarketStatus.OPEN &&
139+
tokenIds.length > 0,
140+
});
141+
142+
const getDisplayPrice = useCallback(
143+
(token: PredictOutcomeToken): number => {
144+
if (!livePricesEnabled) {
145+
return token.price;
146+
}
147+
148+
const liveBestAsk = getPrice(token.id)?.bestAsk;
149+
return isValidPrice(liveBestAsk) ? liveBestAsk : token.price;
150+
},
151+
[getPrice, livePricesEnabled],
152+
);
120153

121154
const handleCardPress = useCallback(() => {
122155
navigation.navigate(Routes.PREDICT.ROOT, {
@@ -265,7 +298,7 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
265298
{game.homeTeam.name}
266299
</Text>
267300
{homeToken && (
268-
<FeaturedCarouselPayoutRow price={homeToken.price} />
301+
<FeaturedCarouselPayoutRow price={getDisplayPrice(homeToken)} />
269302
)}
270303
</Box>
271304

@@ -279,7 +312,7 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
279312
{game.awayTeam.name}
280313
</Text>
281314
{awayToken && (
282-
<FeaturedCarouselPayoutRow price={awayToken.price} />
315+
<FeaturedCarouselPayoutRow price={getDisplayPrice(awayToken)} />
283316
)}
284317
</Box>
285318
</Box>
@@ -298,7 +331,9 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
298331
style={tw.style('font-medium')}
299332
color={TextColor.SuccessDefault}
300333
>
301-
{formatPercentage(Math.round(homeToken.price * 100))}
334+
{formatPercentage(
335+
Math.round(getDisplayPrice(homeToken) * 100),
336+
)}
302337
</Text>
303338
</Button>
304339
</Box>
@@ -315,7 +350,10 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
315350
style={tw.style('font-medium')}
316351
color={TextColor.TextDefault}
317352
>
318-
{strings('predict.outcome_draw')}
353+
{strings('predict.outcome_draw')}{' '}
354+
{formatPercentage(
355+
Math.round(getDisplayPrice(drawToken) * 100),
356+
)}
319357
</Text>
320358
</Button>
321359
</Box>
@@ -333,7 +371,9 @@ const FeaturedCarouselSportCard: React.FC<FeaturedCarouselSportCardProps> = ({
333371
style={tw.style('font-medium')}
334372
color={TextColor.SuccessDefault}
335373
>
336-
{formatPercentage(Math.round(awayToken.price * 100))}
374+
{formatPercentage(
375+
Math.round(getDisplayPrice(awayToken) * 100),
376+
)}
337377
</Text>
338378
</Button>
339379
</Box>

app/components/UI/Predict/components/PredictGameDetailsContent/PredictGameDetailsContent.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
TextVariant,
1313
} from '@metamask/design-system-react-native';
1414
import { useTailwind } from '@metamask/design-system-twrnc-preset';
15-
import React, { useCallback, useMemo } from 'react';
15+
import React, { memo, useCallback, useMemo } from 'react';
1616
import { Pressable, RefreshControl, ScrollView } from 'react-native';
1717
import {
1818
SafeAreaView,
@@ -37,7 +37,9 @@ import { PREDICT_GAME_DETAILS_CONTENT_TEST_IDS } from './PredictGameDetailsConte
3737

3838
const CHIPS_STICKY_INDEX = 2;
3939

40-
const PredictGameDetailsContent: React.FC<PredictGameDetailsContentProps> = ({
40+
const PredictGameDetailsContentComponent: React.FC<
41+
PredictGameDetailsContentProps
42+
> = ({
4143
market,
4244
onBack,
4345
onRefresh,
@@ -231,4 +233,10 @@ const PredictGameDetailsContent: React.FC<PredictGameDetailsContentProps> = ({
231233
);
232234
};
233235

236+
// Memoized so a parent (PredictMarketDetails) re-render driven by its own live
237+
// subscriptions does not re-render this entire subtree when our props are
238+
// unchanged. The screen's live odds updates are driven by this component's own
239+
// hooks instead.
240+
const PredictGameDetailsContent = memo(PredictGameDetailsContentComponent);
241+
234242
export default PredictGameDetailsContent;

0 commit comments

Comments
 (0)