Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
62a73eb
chore: set OTA_VERSION to v7.76.3 for OTA hotfix (release/7.76.3-ota)
metamaskbot May 11, 2026
3082b84
Cherry-picking commits from main to release/7.76.3-ota for PR #29914 …
runway-github[bot] May 12, 2026
0207b3c
[skip ci] Bump version number to 4917
metamaskbot May 12, 2026
11349b0
Cherry-picking commits from main to release/7.76.3-ota for PR #29917 …
runway-github[bot] May 12, 2026
baf7ce4
[skip ci] Bump version number to 4918
metamaskbot May 12, 2026
ca0e791
Cherry-picking commits from main to release/7.76.3-ota for PR #29933 …
runway-github[bot] May 12, 2026
0e7dd19
[skip ci] Bump version number to 4919
metamaskbot May 12, 2026
20f0a97
Cherry-picking commits from main to release/7.76.3-ota for PR #29936 …
runway-github[bot] May 12, 2026
19aca8a
[skip ci] Bump version number to 4920
metamaskbot May 12, 2026
f5c3596
Cherry-picking commits from main to release/7.76.3-ota for PR #29941 …
runway-github[bot] May 12, 2026
1591929
[skip ci] Bump version number to 4921
metamaskbot May 12, 2026
47f901c
Revert "[skip ci] Bump version number to 4921"
chloeYue May 12, 2026
ec96c79
Revert "[skip ci] Bump version number to 4920"
chloeYue May 12, 2026
9c21ba4
Revert "[skip ci] Bump version number to 4919"
chloeYue May 12, 2026
eeaa709
Revert "[skip ci] Bump version number to 4918"
chloeYue May 12, 2026
d5de421
Revert "[skip ci] Bump version number to 4917"
chloeYue May 12, 2026
7af0cd0
chore(release): release-changelog/7.76.3 (#30080)
chloeYue May 13, 2026
b7cbce7
ci: re-enable CI on PRs targeting stable cp-7.76.0 (#29986)
chloeYue May 12, 2026
0dec266
Merge branch 'stable' into release/7.76.3-ota
chloeYue May 13, 2026
f71a265
[skip ci] Bump version number to 4937
metamaskbot May 13, 2026
58c8b05
revert automatic version bump
chloeYue May 13, 2026
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
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [7.76.3]

### Added

- Added Predict transaction publishing hooks to support the Polymarket Deposit Wallet flow. (#29914)
- Added support for depositing to a Polymarket Deposit Wallet in Predict, while preserving legacy Safe behavior for users with existing Polymarket activity. (#29917)
- Added Polymarket Deposit Wallet order placement support in Predict. (#29933)
- Added support for claiming Predict positions through the Polymarket Deposit Wallet. (#29936)

### Fixed

- Disabled Predict withdrawals for Polymarket Deposit Wallet users with a temporary "withdrawals unavailable" bottom sheet, while legacy Safe users keep the existing flow. (#29941)

## [7.76.0]

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

[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.3...HEAD
[7.76.3]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...v7.76.3
[7.76.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.1...v7.76.0
[7.75.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.0...v7.75.1
[7.75.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.74.3...v7.75.0
Expand Down
1 change: 1 addition & 0 deletions app/components/UI/Predict/Predict.testIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export const getPredictSearchSelector = {

export const PredictBalanceSelectorsIDs = {
BALANCE_CARD: 'predict-balance-card',
WITHDRAW_BUTTON: 'predict-balance-withdraw-button',
} as const;

export const PredictBalanceSelectorsText = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
import PredictBalance from './PredictBalance';
import { strings } from '../../../../../../locales/i18n';
import { ButtonVariants } from '../../../../../component-library/components/Buttons/Button';
import { PREDICT_BALANCE_TEST_IDS } from './PredictBalance.testIds';

// Mock React Query
jest.mock('@tanstack/react-query', () => ({
Expand All @@ -30,6 +31,13 @@ jest.mock('../../hooks/usePredictBalance', () => ({
usePredictBalance: (options?: unknown) => mockUsePredictBalance(options),
}));

// Mock usePredictAccountState hook
const mockUsePredictAccountState = jest.fn();
jest.mock('../../hooks/usePredictAccountState', () => ({
usePredictAccountState: (options?: unknown) =>
mockUsePredictAccountState(options),
}));

// Mock usePredictDeposit hook
const mockUsePredictDeposit = jest.fn();
jest.mock('../../hooks/usePredictDeposit', () => ({
Expand Down Expand Up @@ -83,6 +91,15 @@ describe('PredictBalance', () => {
isDepositPending: false,
});

mockUsePredictAccountState.mockReturnValue({
data: {
address: '0x1111111111111111111111111111111111111111',
isDeployed: true,
walletType: 'safe',
},
isLoading: false,
});

mockUsePredictWithdraw.mockReturnValue({
withdraw: jest.fn(),
});
Expand Down Expand Up @@ -347,7 +364,7 @@ describe('PredictBalance', () => {
expect(mockDeposit).toHaveBeenCalled();
});

it('calls withdraw directly when Withdraw button is pressed', () => {
it('calls withdraw directly when Withdraw button is pressed for Safe users', () => {
// Arrange
const mockWithdraw = jest.fn();
mockUsePredictBalance.mockReturnValue({
Expand All @@ -369,6 +386,83 @@ describe('PredictBalance', () => {
expect(mockWithdraw).toHaveBeenCalledTimes(1);
expect(mockExecuteGuardedAction).not.toHaveBeenCalled();
});

it('calls temporary unavailable handler instead of withdrawing for Deposit Wallet users', () => {
// Arrange
const mockWithdraw = jest.fn();
const mockOnDepositWalletWithdrawPress = jest.fn();
mockUsePredictBalance.mockReturnValue({
data: 100,
isLoading: false,
});
mockUsePredictAccountState.mockReturnValue({
data: {
address: '0x2222222222222222222222222222222222222222',
isDeployed: true,
walletType: 'deposit-wallet',
},
isLoading: false,
});
mockUsePredictWithdraw.mockReturnValue({
withdraw: mockWithdraw,
});

// Act
const { getByText } = renderWithProvider(
<PredictBalance
onDepositWalletWithdrawPress={mockOnDepositWalletWithdrawPress}
/>,
{
state: initialState,
},
);
const withdrawButton = getByText(/Withdraw/i);
fireEvent.press(withdrawButton);

// Assert
expect(mockWithdraw).not.toHaveBeenCalled();
expect(mockOnDepositWalletWithdrawPress).toHaveBeenCalledTimes(1);
});

it('disables Withdraw while account state is unavailable', () => {
// Arrange
const mockWithdraw = jest.fn();
const mockOnDepositWalletWithdrawPress = jest.fn();
mockUsePredictBalance.mockReturnValue({
data: 100,
isLoading: false,
});
mockUsePredictAccountState.mockReturnValue({
data: undefined,
isLoading: true,
});
mockUsePredictWithdraw.mockReturnValue({
withdraw: mockWithdraw,
});

// Act
const { getByTestId, UNSAFE_getByProps } = renderWithProvider(
<PredictBalance
onDepositWalletWithdrawPress={mockOnDepositWalletWithdrawPress}
/>,
{
state: initialState,
},
);
const withdrawButton = getByTestId(
PREDICT_BALANCE_TEST_IDS.WITHDRAW_BUTTON,
);
fireEvent.press(withdrawButton);

// Assert
const disabledWithdrawButton = UNSAFE_getByProps({
testID: PREDICT_BALANCE_TEST_IDS.WITHDRAW_BUTTON,
isDisabled: true,
});
expect(disabledWithdrawButton.props.isDisabled).toBe(true);
expect(mockWithdraw).not.toHaveBeenCalled();
expect(mockOnDepositWalletWithdrawPress).not.toHaveBeenCalled();
});
});

describe('balance refresh', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export const PREDICT_BALANCE_TEST_IDS = {
SKELETON: 'predict-balance-card-skeleton',
CARD: 'predict-balance-card',
WITHDRAW_BUTTON: 'predict-balance-withdraw-button',
WITHDRAW_UNAVAILABLE_SHEET: 'predict-withdraw-unavailable-sheet',
WITHDRAW_UNAVAILABLE_TITLE: 'predict-withdraw-unavailable-title',
WITHDRAW_UNAVAILABLE_DESCRIPTION: 'predict-withdraw-unavailable-description',
WITHDRAW_UNAVAILABLE_CLOSE_BUTTON:
'predict-withdraw-unavailable-close-button',
WITHDRAW_UNAVAILABLE_GOT_IT_BUTTON:
'predict-withdraw-unavailable-got-it-button',
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,20 @@ import { formatPrice } from '../../utils/format';
import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
import { PredictNavigationParamList } from '../../types/navigation';
import { usePredictWithdraw } from '../../hooks/usePredictWithdraw';
import { usePredictAccountState } from '../../hooks/usePredictAccountState';
import { PredictEventValues } from '../../constants/eventNames';
import { PREDICT_BALANCE_TEST_IDS } from './PredictBalance.testIds';

// This is a temporary component that will be removed when the deposit flow is fully implemented
interface PredictBalanceProps {
onLayout?: (height: number) => void;
onDepositWalletWithdrawPress?: () => void;
}

const PredictBalance: React.FC<PredictBalanceProps> = ({ onLayout }) => {
const PredictBalance: React.FC<PredictBalanceProps> = ({
onLayout,
onDepositWalletWithdrawPress,
}) => {
const tw = useTailwind();
const privacyMode = useSelector(selectPrivacyMode);

Expand All @@ -64,6 +69,11 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({ onLayout }) => {

const isAddingFunds = isDepositPending;
const hasBalance = balance > 0;
const { data: accountState } = usePredictAccountState({
enabled: hasBalance,
});
const walletType = accountState?.walletType;
const isWithdrawDisabled = hasBalance && !walletType;

useEffect(() => {
if (!isDepositPending) {
Expand All @@ -87,8 +97,21 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({ onLayout }) => {
}, [deposit, executeGuardedAction]);

const handleWithdraw = useCallback(() => {
// Do not proceed until account state is loaded; otherwise Deposit Wallet
// users can bypass the temporary guard during the query window.
if (!walletType) {
return;
}

// Temporary Deposit Wallet migration guard. Remove this branch and sheet
// once Deposit Wallet withdrawals are implemented.
if (walletType === 'deposit-wallet') {
onDepositWalletWithdrawPress?.();
return;
}

withdraw();
}, [withdraw]);
}, [onDepositWalletWithdrawPress, walletType, withdraw]);

if (isLoading) {
return (
Expand Down Expand Up @@ -202,6 +225,8 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({ onLayout }) => {
style={tw.style('flex-1')}
label={strings('predict.deposit.withdraw')}
onPress={handleWithdraw}
isDisabled={isWithdrawDisabled}
testID={PREDICT_BALANCE_TEST_IDS.WITHDRAW_BUTTON}
/>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react-native';
import { strings } from '../../../../../../locales/i18n';
import PredictWithdrawUnavailableSheet from './PredictWithdrawUnavailableSheet';
import { PREDICT_BALANCE_TEST_IDS } from '../PredictBalance/PredictBalance.testIds';

let mockIsVisible = true;
const mockCloseSheet = jest.fn();
const mockGetRefHandlers = jest.fn(() => ({
onOpenBottomSheet: jest.fn(),
onCloseBottomSheet: jest.fn(),
}));

jest.mock('../../hooks/usePredictBottomSheet', () => ({
usePredictBottomSheet: () => ({
sheetRef: { current: null },
isVisible: mockIsVisible,
closeSheet: mockCloseSheet,
handleSheetClosed: jest.fn(),
getRefHandlers: mockGetRefHandlers,
}),
}));

describe('PredictWithdrawUnavailableSheet', () => {
beforeEach(() => {
jest.clearAllMocks();
mockIsVisible = true;
});

it('renders null when closed', () => {
mockIsVisible = false;

const { queryByTestId } = render(<PredictWithdrawUnavailableSheet />);

expect(
queryByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_SHEET),
).toBeNull();
});

it('renders the temporary Deposit Wallet withdraw unavailable note', () => {
const { getByTestId } = render(<PredictWithdrawUnavailableSheet />);

expect(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_SHEET),
).toBeOnTheScreen();
expect(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_TITLE),
).toHaveTextContent(strings('predict.withdraw.unavailable_title'));
expect(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_DESCRIPTION),
).toHaveTextContent(strings('predict.withdraw.unavailable_description'));
expect(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_GOT_IT_BUTTON),
).toHaveTextContent(strings('predict.withdraw.unavailable_got_it'));
});

it('dismisses when Got it is pressed', () => {
const { getByTestId } = render(<PredictWithdrawUnavailableSheet />);

fireEvent.press(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_GOT_IT_BUTTON),
);

expect(mockCloseSheet).toHaveBeenCalledTimes(1);
});

it('dismisses when close is pressed', () => {
const { getByTestId } = render(<PredictWithdrawUnavailableSheet />);

fireEvent.press(
getByTestId(PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_CLOSE_BUTTON),
);

expect(mockCloseSheet).toHaveBeenCalledTimes(1);
});
});
Loading
Loading