Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ describe('PredictPreviewSheetContext', () => {

fireEvent.press(screen.getByTestId('open-buy'));

expect(mockNavigate).toHaveBeenCalledWith(
Routes.PREDICT.MODALS.BUY_PREVIEW,
buyParams,
);
expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.BUY_PREVIEW,
params: buyParams,
});
expect(
screen.queryByTestId('predict-buy-preview-sheet'),
).not.toBeOnTheScreen();
Expand All @@ -274,10 +274,10 @@ describe('PredictPreviewSheetContext', () => {

fireEvent.press(screen.getByTestId('open-sell'));

expect(mockNavigate).toHaveBeenCalledWith(
Routes.PREDICT.MODALS.SELL_PREVIEW,
sellParams,
);
expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.SELL_PREVIEW,
params: sellParams,
});
expect(
screen.queryByTestId('predict-sell-preview-sheet'),
).not.toBeOnTheScreen();
Expand Down Expand Up @@ -557,6 +557,89 @@ describe('PredictPreviewSheetContext', () => {

expect(isPredictSheetProviderMounted()).toBe(false);
});

it('returns false when provider is mounted with disableBottomSheet=true', () => {
const { unmount } = render(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

expect(isPredictSheetProviderMounted()).toBe(false);

unmount();
});

it('returns false after unmounting a disableBottomSheet provider', () => {
const { unmount } = render(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

unmount();

expect(isPredictSheetProviderMounted()).toBe(false);
});
});

describe('disableBottomSheet prop', () => {
it('navigates to BUY_PREVIEW instead of opening sheet when disableBottomSheet=true and flag is ON', () => {
render(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

fireEvent.press(screen.getByTestId('open-buy'));

expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.BUY_PREVIEW,
params: buyParams,
});
expect(
screen.queryByTestId('predict-buy-preview-sheet'),
).not.toBeOnTheScreen();
});

it('navigates to SELL_PREVIEW instead of opening sheet when disableBottomSheet=true and flag is ON', () => {
render(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

fireEvent.press(screen.getByTestId('open-sell'));

expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.SELL_PREVIEW,
params: sellParams,
});
expect(
screen.queryByTestId('predict-sell-preview-sheet'),
).not.toBeOnTheScreen();
});

it('does not auto-reopen buy sheet when disableBottomSheet=true', () => {
const { rerender } = render(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

fireEvent.press(screen.getByTestId('open-buy'));

mockActiveOrder = { error: 'order/failed' };
rerender(
<PredictPreviewSheetProvider disableBottomSheet>
<TestConsumer />
</PredictPreviewSheetProvider>,
);

expect(
screen.queryByTestId('predict-buy-preview-sheet'),
).not.toBeOnTheScreen();
});
});

describe('clearOrderError on dismiss', () => {
Expand Down
51 changes: 37 additions & 14 deletions app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ import { PredictDismissalMethod } from '../constants/eventNames';
import { parseAnalyticsProperties } from '../utils/analytics';

let _providerMounted = false;
let _providerInSheetMode = false;

/**
* Returns whether `PredictPreviewSheetProvider` is currently mounted somewhere
* in the tree. Used by `usePredictToastRegistrations` to decide whether to
* suppress the order-failure toast (the provider auto-reopens the sheet with
* an inline error banner instead).
* Returns whether `PredictPreviewSheetProvider` is mounted AND will actually
* open a bottom sheet (i.e. not in disableBottomSheet mode). Used by
* `usePredictToastRegistrations` to suppress the order-failure toast when the
* provider will auto-reopen the sheet with an inline error banner instead.
*/
export function isPredictSheetProviderMounted(): boolean {
return _providerMounted;
return _providerMounted && _providerInSheetMode;
}

const SellSheetHeader: React.FC<{ params: PredictSellPreviewParams }> = ({
Expand Down Expand Up @@ -150,11 +151,24 @@ export const usePredictPreviewSheet = (): PredictPreviewSheetContextValue => {

interface PredictPreviewSheetProviderProps {
children: React.ReactNode;
/**
* When true, always navigate to the full-screen bet slip instead of opening
* the bottom sheet. Required when the provider is rendered inside
* HomepageDiscoveryTabs, where the sheet is obscured by the tab layout.
*
* This prop exists solely to support the Hub Page Discovery Tabs A/B test
* (LD flag: `coreMCU589AbtestHubPageDiscoveryTabs`). If that feature is
* scrapped or fully rolled out and this layout is no longer needed, this prop
* can be removed along with the HomepageDiscoveryTabs component.
*
* Contact @metamask-core-mobile-ux for questions about the flag or rollout.
*/
disableBottomSheet?: boolean;
}

export const PredictPreviewSheetProvider: React.FC<
PredictPreviewSheetProviderProps
> = ({ children }) => {
> = ({ children, disableBottomSheet = false }) => {
const navigation = useNavigation();
const bottomSheetEnabled = useSelector(selectPredictBottomSheetEnabledFlag);
const payWithAnyTokenEnabled = useSelector(
Expand Down Expand Up @@ -191,36 +205,44 @@ export const PredictPreviewSheetProvider: React.FC<

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigate via ROOT so the screen resolves correctly

useEffect(() => {
_providerMounted = true;
_providerInSheetMode = !disableBottomSheet;
return () => {
_providerMounted = false;
_providerInSheetMode = false;
};
}, []);
}, [disableBottomSheet]);

const openBuySheet = useCallback(
(params: PredictBuyPreviewParams) => {
lastBuyParamsRef.current = params;
if (bottomSheetEnabled) {
if (bottomSheetEnabled && !disableBottomSheet) {
setBuyParams(params);
buyNonceRef.current += 1;
setBuyNonce(buyNonceRef.current);
} else {
navigation.navigate(Routes.PREDICT.MODALS.BUY_PREVIEW, params);
navigation.navigate(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.BUY_PREVIEW,
params,
});
}
},
[bottomSheetEnabled, navigation],
[bottomSheetEnabled, disableBottomSheet, navigation],
);

const openSellSheet = useCallback(
(params: PredictSellPreviewParams) => {
if (bottomSheetEnabled) {
if (bottomSheetEnabled && !disableBottomSheet) {
setSellParams(params);
sellNonceRef.current += 1;
setSellNonce(sellNonceRef.current);
} else {
navigation.navigate(Routes.PREDICT.MODALS.SELL_PREVIEW, params);
navigation.navigate(Routes.PREDICT.ROOT, {
screen: Routes.PREDICT.MODALS.SELL_PREVIEW,
params,
});
}
},
[bottomSheetEnabled, navigation],
[bottomSheetEnabled, disableBottomSheet, navigation],
);

useEffect(() => {
Expand All @@ -244,6 +266,7 @@ export const PredictPreviewSheetProvider: React.FC<

if (
bottomSheetEnabled &&
!disableBottomSheet &&
activeOrder?.error &&
!buyParams &&
lastBuyParamsRef.current &&
Expand All @@ -255,7 +278,7 @@ export const PredictPreviewSheetProvider: React.FC<
buyNonceRef.current += 1;
setBuyNonce(buyNonceRef.current);
}
}, [activeOrder?.error, buyParams, bottomSheetEnabled]);
}, [activeOrder?.error, buyParams, bottomSheetEnabled, disableBottomSheet]);

const BuyComponent = useMemo(
() => (payWithAnyTokenEnabled ? PredictBuyWithAnyToken : PredictBuyPreview),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ import React from 'react';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { PredictMarket } from '../../types';
import PredictBuyPreview from './PredictBuyPreview';
import PredictBuyPreview, {
predictBuyPreviewDismissedViaBackRef,
predictBuyPreviewOrderInitiatedRef,
} from './PredictBuyPreview';
import { PredictNavigationParamList } from '../../types/navigation';
import { PredictEventValues } from '../../constants/eventNames';
import {
PredictEventValues,
PredictDismissalMethod,
} from '../../constants/eventNames';

import { POLYMARKET_PROVIDER_ID } from '../../providers/polymarket/constants';
// Mock Engine
Expand Down Expand Up @@ -225,14 +231,22 @@ const mockRoute: RouteProp<PredictNavigationParamList, 'PredictBuyPreview'> = {
},
};

let mockBeforeRemoveCallback: (() => void) | null = null;
const mockAddListener = jest.fn((event: string, cb: () => void) => {
if (event === 'beforeRemove') {
mockBeforeRemoveCallback = cb;
}
return jest.fn();
});

const mockNavigation: NavigationProp<PredictNavigationParamList> = {
goBack: mockGoBack,
dispatch: mockDispatch,
navigate: jest.fn(),
reset: jest.fn(),
setParams: jest.fn(),
setOptions: jest.fn(),
addListener: jest.fn(),
addListener: mockAddListener,
removeListener: jest.fn(),
canGoBack: jest.fn(),
isFocused: jest.fn(),
Expand Down Expand Up @@ -281,6 +295,8 @@ describe('PredictBuyPreview', () => {
mockEstimatedPoints = null;
mockRewardsError = false;

mockBeforeRemoveCallback = null;

// Setup default mocks
mockUseNavigation.mockReturnValue(mockNavigation);
mockUseRoute.mockReturnValue(mockRoute);
Expand Down Expand Up @@ -2529,4 +2545,60 @@ describe('PredictBuyPreview', () => {
expect(screen.getByText('To win')).toBeOnTheScreen();
});
});

describe('beforeRemove dismiss tracking (screen mode)', () => {
const trackBetslipDismissed =
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../../../../../core/Engine').context.PredictController
.trackBetslipDismissed;

beforeEach(() => {
predictBuyPreviewDismissedViaBackRef.current = false;
predictBuyPreviewOrderInitiatedRef.current = false;
});

it('registers a beforeRemove listener in screen mode', () => {
renderWithProvider(<PredictBuyPreview />, { state: initialState });

expect(mockAddListener).toHaveBeenCalledWith(
'beforeRemove',
expect.any(Function),
);
});

it('tracks swipe dismissal via beforeRemove when back ref is false', () => {
renderWithProvider(<PredictBuyPreview />, { state: initialState });

predictBuyPreviewDismissedViaBackRef.current = false;
mockBeforeRemoveCallback?.();

expect(trackBetslipDismissed).toHaveBeenCalledWith(
expect.objectContaining({
dismissalMethod: PredictDismissalMethod.SWIPE,
}),
);
});

it('tracks back-button dismissal via beforeRemove when back ref is true', () => {
renderWithProvider(<PredictBuyPreview />, { state: initialState });

predictBuyPreviewDismissedViaBackRef.current = true;
mockBeforeRemoveCallback?.();

expect(trackBetslipDismissed).toHaveBeenCalledWith(
expect.objectContaining({
dismissalMethod: PredictDismissalMethod.BACK_BUTTON,
}),
);
});

it('does not track dismissal when order was initiated', () => {
renderWithProvider(<PredictBuyPreview />, { state: initialState });

predictBuyPreviewOrderInitiatedRef.current = true;
mockBeforeRemoveCallback?.();

expect(trackBetslipDismissed).not.toHaveBeenCalled();
});
});
});
Loading
Loading