Skip to content

Commit 7e988fc

Browse files
authored
feat(predict): show FAK partial fill note in price details breakdown cp-7.69.0 (MetaMask#27218)
## **Description** When FAK (Fill-And-Kill) orders are enabled, the PredictFeeBreakdownSheet now displays a footnote below the Total row explaining that prices assume a fully filled order and actual amounts may vary if the order is only partially filled. Additionally, the component was refactored to use `@metamask/design-system-react-native` primitives (`Text`, `TextColor`, `TextVariant`, `FontWeight`) instead of the local `component-library` Text, aligning with the project's UI development guidelines. **Changes:** - Created `selectPredictFakOrdersEnabledFlag` Redux selector for the `predictFakOrders` version-gated remote feature flag - Added `fakOrdersEnabled` prop to `PredictFeeBreakdownSheet` — conditionally renders a partial fill note - Wired `PredictBuyPreview` to read the selector and pass it to the sheet - Migrated `Text`, `TextColor`, `TextVariant` imports from `component-library` to `@metamask/design-system-react-native` - Replaced `TextVariant.BodyMDBold` with `TextVariant.BodyMd` + `fontWeight={FontWeight.Bold}` ## **Changelog** CHANGELOG entry: Added a note in prediction price details explaining that prices assume a fully filled order when FAK orders are enabled ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/PRED-722 ## **Manual testing steps** ```gherkin Feature: FAK partial fill note in price details Scenario: user sees partial fill note when FAK orders flag is enabled Given the predictFakOrders remote feature flag is enabled And the user is on the buy prediction preview screen When user taps on the price details row to open the fee breakdown sheet Then a note is displayed below the Total row reading "Prices shown assume your order is fully filled. Actual amounts may vary if the order is only partially filled." Scenario: user does not see partial fill note when FAK flag is disabled Given the predictFakOrders remote feature flag is disabled And the user is on the buy prediction preview screen When user taps on the price details row to open the fee breakdown sheet Then no partial fill note is displayed below the Total row ``` ## **Screenshots/Recordings** ### **Before** <!-- Price details sheet shows Total as the last row --> <img width="420" height="865" alt="Screenshot 2026-03-09 at 1 19 11 PM" src="https://github.com/user-attachments/assets/a0c1110c-1840-401f-8807-bebe018253cd" /> ### **After** <!-- Price details sheet shows a footnote below Total when FAK is enabled --> <img width="421" height="854" alt="Screenshot 2026-03-09 at 1 20 36 PM" src="https://github.com/user-attachments/assets/056237e0-37ea-4b8e-8501-5b82befafd27" /> ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. ## **Pre-merge reviewer checklist** - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI/feature-flag change that adds conditional copy and a new selector; main risk is incorrect flag evaluation or unintended note display. > > **Overview** > **Predict price details now conditionally shows a FAK partial-fill disclaimer** when the remote `predictFakOrders` version-gated flag is enabled. > > This introduces `selectPredictFakOrdersEnabledFlag`, wires it into `PredictBuyPreview` to pass `fakOrdersEnabled` to `PredictFeeBreakdownSheet`, and updates the sheet to render the new footnote (plus a small refactor to `@metamask/design-system-react-native` `Text` primitives via a reusable `FeeRow`). Tests and `en.json` strings are updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9bf73d0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 033777f commit 7e988fc

6 files changed

Lines changed: 288 additions & 58 deletions

File tree

app/components/UI/Predict/components/PredictFeeBreakdownSheet/PredictFeeBreakdownSheet.test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ jest.mock('../../../../../../locales/i18n', () => ({
8787
if (key === 'predict.fee_summary.close') {
8888
return 'Close';
8989
}
90+
if (key === 'predict.fee_summary.fak_partial_fill_note') {
91+
return 'Prices shown assume your order is fully filled. Actual amounts may vary if the order is only partially filled.';
92+
}
9093
return key;
9194
}),
9295
}));
@@ -325,4 +328,55 @@ describe('PredictFeeBreakdownSheet', () => {
325328
render(<TestComponent />);
326329
});
327330
});
331+
332+
describe('FAK partial fill note', () => {
333+
it('displays partial fill note when fakOrdersEnabled is true', () => {
334+
const TestComponent = () => {
335+
const ref = useRef<BottomSheetRef>(null);
336+
return (
337+
<PredictFeeBreakdownSheet
338+
ref={ref}
339+
{...defaultProps}
340+
fakOrdersEnabled
341+
/>
342+
);
343+
};
344+
345+
const { getByTestId } = render(<TestComponent />);
346+
347+
expect(getByTestId('predict-fak-partial-fill-note')).toBeOnTheScreen();
348+
});
349+
350+
it('does not display partial fill note when fakOrdersEnabled is false', () => {
351+
const TestComponent = () => {
352+
const ref = useRef<BottomSheetRef>(null);
353+
return (
354+
<PredictFeeBreakdownSheet
355+
ref={ref}
356+
{...defaultProps}
357+
fakOrdersEnabled={false}
358+
/>
359+
);
360+
};
361+
362+
const { queryByTestId } = render(<TestComponent />);
363+
364+
expect(
365+
queryByTestId('predict-fak-partial-fill-note'),
366+
).not.toBeOnTheScreen();
367+
});
368+
369+
it('does not display partial fill note by default', () => {
370+
const TestComponent = () => {
371+
const ref = useRef<BottomSheetRef>(null);
372+
return <PredictFeeBreakdownSheet ref={ref} {...defaultProps} />;
373+
};
374+
375+
const { queryByTestId } = render(<TestComponent />);
376+
377+
expect(
378+
queryByTestId('predict-fak-partial-fill-note'),
379+
).not.toBeOnTheScreen();
380+
});
381+
});
328382
});

app/components/UI/Predict/components/PredictFeeBreakdownSheet/PredictFeeBreakdownSheet.tsx

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
11
import React, { forwardRef } from 'react';
2-
import { Box } from '@metamask/design-system-react-native';
2+
import {
3+
Box,
4+
FontWeight,
5+
Text,
6+
TextColor,
7+
TextVariant,
8+
} from '@metamask/design-system-react-native';
39
import BottomSheet, {
410
BottomSheetRef,
511
} from '../../../../../component-library/components/BottomSheets/BottomSheet';
612
import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader';
7-
import Text, {
8-
TextColor,
9-
TextVariant,
10-
} from '../../../../../component-library/components/Texts/Text';
1113
import { strings } from '../../../../../../locales/i18n';
1214
import { formatPrice } from '../../utils/format';
1315
import { SLIPPAGE_BUY } from '../../providers/polymarket/constants';
1416

17+
interface FeeRowProps {
18+
title: string;
19+
description: string;
20+
amount: string;
21+
}
22+
23+
const FeeRow = ({ title, description, amount }: FeeRowProps) => (
24+
<>
25+
<Box twClassName="flex-row items-start py-4">
26+
<Box twClassName="flex-1 pr-4 gap-1">
27+
<Text color={TextColor.TextDefault} variant={TextVariant.BodyMd}>
28+
{title}
29+
</Text>
30+
<Text color={TextColor.TextAlternative} variant={TextVariant.BodyXs}>
31+
{description}
32+
</Text>
33+
</Box>
34+
<Text color={TextColor.TextDefault} variant={TextVariant.BodyMd}>
35+
{amount}
36+
</Text>
37+
</Box>
38+
<Box twClassName="border-t border-muted" />
39+
</>
40+
);
41+
1542
interface PredictFeeBreakdownSheetProps {
1643
providerFee: number;
1744
metamaskFee: number;
@@ -20,6 +47,7 @@ interface PredictFeeBreakdownSheetProps {
2047
betAmount: number;
2148
total: number;
2249
onClose?: () => void;
50+
fakOrdersEnabled?: boolean;
2351
}
2452

2553
const PredictFeeBreakdownSheet = forwardRef<
@@ -35,72 +63,65 @@ const PredictFeeBreakdownSheet = forwardRef<
3563
betAmount,
3664
total,
3765
onClose,
66+
fakOrdersEnabled = false,
3867
},
3968
ref,
4069
) => (
4170
<BottomSheet ref={ref} onClose={onClose} shouldNavigateBack={false}>
4271
<SheetHeader title={strings('predict.fee_summary.price_details')} />
4372
<Box twClassName="px-4 pb-6 flex-col">
44-
<Box twClassName="flex-row items-start py-4">
45-
<Box twClassName="flex-1 pr-4 gap-1">
46-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
47-
{strings('predict.fee_summary.prediction_order')}
48-
</Text>
49-
<Text color={TextColor.Alternative} variant={TextVariant.BodyXS}>
50-
{strings('predict.fee_summary.prediction_order_description', {
51-
count: contractCount.toFixed(2),
52-
price: formatPrice(sharePrice, { maximumDecimals: 2 }),
53-
slippage: Math.round(SLIPPAGE_BUY * 100),
54-
})}
55-
</Text>
56-
</Box>
57-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
58-
{formatPrice(betAmount, { maximumDecimals: 2 })}
59-
</Text>
60-
</Box>
61-
62-
<Box twClassName="border-t border-muted" />
63-
64-
<Box twClassName="flex-row items-start py-4">
65-
<Box twClassName="flex-1 pr-4 gap-1">
66-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
67-
{strings('predict.fee_summary.metamask_fee')}
68-
</Text>
69-
<Text color={TextColor.Alternative} variant={TextVariant.BodyXS}>
70-
{strings('predict.fee_summary.metamask_fee_description')}
71-
</Text>
72-
</Box>
73-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
74-
{formatPrice(metamaskFee, { maximumDecimals: 2 })}
75-
</Text>
76-
</Box>
73+
<FeeRow
74+
title={strings('predict.fee_summary.prediction_order')}
75+
description={strings(
76+
'predict.fee_summary.prediction_order_description',
77+
{
78+
count: contractCount.toFixed(2),
79+
price: formatPrice(sharePrice, { maximumDecimals: 2 }),
80+
slippage: Math.round(SLIPPAGE_BUY * 100),
81+
},
82+
)}
83+
amount={formatPrice(betAmount, { maximumDecimals: 2 })}
84+
/>
7785

78-
<Box twClassName="border-t border-muted" />
86+
<FeeRow
87+
title={strings('predict.fee_summary.metamask_fee')}
88+
description={strings('predict.fee_summary.metamask_fee_description')}
89+
amount={formatPrice(metamaskFee, { maximumDecimals: 2 })}
90+
/>
7991

80-
<Box twClassName="flex-row items-start py-4">
81-
<Box twClassName="flex-1 pr-4 gap-1">
82-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
83-
{strings('predict.fee_summary.exchange_fee')}
84-
</Text>
85-
<Text color={TextColor.Alternative} variant={TextVariant.BodyXS}>
86-
{strings('predict.fee_summary.exchange_fee_description')}
87-
</Text>
88-
</Box>
89-
<Text color={TextColor.Default} variant={TextVariant.BodyMD}>
90-
{formatPrice(providerFee, { maximumDecimals: 2 })}
91-
</Text>
92-
</Box>
93-
94-
<Box twClassName="border-t border-muted" />
92+
<FeeRow
93+
title={strings('predict.fee_summary.exchange_fee')}
94+
description={strings('predict.fee_summary.exchange_fee_description')}
95+
amount={formatPrice(providerFee, { maximumDecimals: 2 })}
96+
/>
9597

9698
<Box twClassName="flex-row justify-between items-center pt-4">
97-
<Text color={TextColor.Default} variant={TextVariant.BodyMDBold}>
99+
<Text
100+
color={TextColor.TextDefault}
101+
variant={TextVariant.BodyMd}
102+
fontWeight={FontWeight.Bold}
103+
>
98104
{strings('predict.fee_summary.total')}
99105
</Text>
100-
<Text color={TextColor.Default} variant={TextVariant.BodyMDBold}>
106+
<Text
107+
color={TextColor.TextDefault}
108+
variant={TextVariant.BodyMd}
109+
fontWeight={FontWeight.Bold}
110+
>
101111
{formatPrice(total, { maximumDecimals: 2 })}
102112
</Text>
103113
</Box>
114+
115+
{fakOrdersEnabled && (
116+
<Text
117+
testID="predict-fak-partial-fill-note"
118+
color={TextColor.TextAlternative}
119+
variant={TextVariant.BodyXs}
120+
twClassName="mt-3"
121+
>
122+
{strings('predict.fee_summary.fak_partial_fill_note')}
123+
</Text>
124+
)}
104125
</Box>
105126
</BottomSheet>
106127
),

app/components/UI/Predict/selectors/featureFlags/index.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
selectPredictEnabledFlag,
3+
selectPredictFakOrdersEnabledFlag,
34
selectPredictFeeCollectionFlag,
45
selectPredictHotTabFlag,
56
} from '.';
@@ -791,4 +792,136 @@ describe('Predict Feature Flag Selectors', () => {
791792
expect(result.enabled).toBe(false);
792793
});
793794
});
795+
796+
describe('selectPredictFakOrdersEnabledFlag', () => {
797+
it('returns true when remote flag is enabled and version check passes', () => {
798+
mockHasMinimumRequiredVersion.mockReturnValue(true);
799+
const state = {
800+
engine: {
801+
backgroundState: {
802+
RemoteFeatureFlagController: {
803+
remoteFeatureFlags: {
804+
predictFakOrders: {
805+
enabled: true,
806+
minimumVersion: '1.0.0',
807+
},
808+
},
809+
cacheTimestamp: 0,
810+
},
811+
},
812+
},
813+
};
814+
815+
const result = selectPredictFakOrdersEnabledFlag(state);
816+
817+
expect(result).toBe(true);
818+
});
819+
820+
it('returns false when remote flag is disabled', () => {
821+
mockHasMinimumRequiredVersion.mockReturnValue(true);
822+
const state = {
823+
engine: {
824+
backgroundState: {
825+
RemoteFeatureFlagController: {
826+
remoteFeatureFlags: {
827+
predictFakOrders: {
828+
enabled: false,
829+
minimumVersion: '1.0.0',
830+
},
831+
},
832+
cacheTimestamp: 0,
833+
},
834+
},
835+
},
836+
};
837+
838+
const result = selectPredictFakOrdersEnabledFlag(state);
839+
840+
expect(result).toBe(false);
841+
});
842+
843+
it('returns false when app version is below minimum required version', () => {
844+
mockHasMinimumRequiredVersion.mockReturnValue(false);
845+
const state = {
846+
engine: {
847+
backgroundState: {
848+
RemoteFeatureFlagController: {
849+
remoteFeatureFlags: {
850+
predictFakOrders: {
851+
enabled: true,
852+
minimumVersion: '99.0.0',
853+
},
854+
},
855+
cacheTimestamp: 0,
856+
},
857+
},
858+
},
859+
};
860+
861+
const result = selectPredictFakOrdersEnabledFlag(state);
862+
863+
expect(result).toBe(false);
864+
});
865+
866+
it('defaults to false when remote flag is null', () => {
867+
const state = {
868+
engine: {
869+
backgroundState: {
870+
RemoteFeatureFlagController: {
871+
remoteFeatureFlags: {
872+
predictFakOrders: null,
873+
},
874+
cacheTimestamp: 0,
875+
},
876+
},
877+
},
878+
};
879+
880+
const result = selectPredictFakOrdersEnabledFlag(state);
881+
882+
expect(result).toBe(false);
883+
});
884+
885+
it('defaults to false when remote feature flags are empty', () => {
886+
const result = selectPredictFakOrdersEnabledFlag(mockedEmptyFlagsState);
887+
888+
expect(result).toBe(false);
889+
});
890+
891+
it('defaults to false when controller is undefined', () => {
892+
const state = {
893+
engine: {
894+
backgroundState: {
895+
RemoteFeatureFlagController: undefined,
896+
},
897+
},
898+
};
899+
900+
const result = selectPredictFakOrdersEnabledFlag(state);
901+
902+
expect(result).toBe(false);
903+
});
904+
905+
it('defaults to false when remote flag is invalid', () => {
906+
const state = {
907+
engine: {
908+
backgroundState: {
909+
RemoteFeatureFlagController: {
910+
remoteFeatureFlags: {
911+
predictFakOrders: {
912+
enabled: 'invalid',
913+
minimumVersion: 123,
914+
},
915+
},
916+
cacheTimestamp: 0,
917+
},
918+
},
919+
},
920+
};
921+
922+
const result = selectPredictFakOrdersEnabledFlag(state);
923+
924+
expect(result).toBe(false);
925+
});
926+
});
794927
});

0 commit comments

Comments
 (0)