Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,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 @@ -304,10 +304,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 @@ -708,6 +708,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
48 changes: 38 additions & 10 deletions app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { PredictDismissalMethod } from '../constants/eventNames';
import { parseAnalyticsProperties } from '../utils/analytics';

let _providerMounted = false;
let _providerInSheetMode = false;

/**
* Returns whether `PredictPreviewSheetProvider` is currently mounted somewhere
Expand All @@ -61,7 +62,7 @@ let _providerMounted = false;
* toast would be a duplicate.
*/
export function isPredictSheetProviderMounted(): boolean {
return _providerMounted;
return _providerMounted && _providerInSheetMode;
}

const SellSheetHeader: React.FC<{ params: PredictSellPreviewParams }> = ({
Expand Down Expand Up @@ -156,11 +157,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 @@ -207,40 +221,48 @@ 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;
if (clearErrorTimerRef.current) {
clearTimeout(clearErrorTimerRef.current);
clearErrorTimerRef.current = null;
}
};
}, []);
}, [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 Down Expand Up @@ -275,7 +297,12 @@ export const PredictPreviewSheetProvider: React.FC<
}
// Only for the bottom-sheet flow, with the slip closed, and only if we
// know which params to reopen with.
if (!bottomSheetEnabled || buyParams || !lastBuyParamsRef.current) {
if (
!bottomSheetEnabled ||
disableBottomSheet ||
buyParams ||
!lastBuyParamsRef.current
) {
return;
}

Expand Down Expand Up @@ -332,6 +359,7 @@ export const PredictPreviewSheetProvider: React.FC<
activeOrder?.error,
buyParams,
bottomSheetEnabled,
disableBottomSheet,
openBuySheet,
clearOrderError,
]);
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,76 @@ 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();
});

it('resets dismissedViaBackRef on mount so a previous back-button session does not bleed into next swipe', () => {
// Simulate a stale true value left over from a previous session
predictBuyPreviewDismissedViaBackRef.current = true;

renderWithProvider(<PredictBuyPreview />, { state: initialState });

// ref should be cleared on mount — swipe dismissal must not be misclassified
mockBeforeRemoveCallback?.();

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