diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index 9a7ccd36307..b743f576615 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -86,7 +86,10 @@ import SendTransaction from '../../UI/Ramp/Aggregator/Views/SendTransaction';
import TabBar from '../../../component-library/components/Navigation/TabBar';
///: BEGIN:ONLY_INCLUDE_IF(snaps)
import { SnapsSettingsList } from '../../Views/Snaps/SnapsSettingsList';
-import { SnapSettings } from '../../Views/Snaps/SnapSettings';
+import {
+ SnapSettings,
+ ALLOWED_CAPABILITIES as SNAPS_SETTINGS_ROUTE_ALLOWED_CAPABILITIES,
+} from '../../Views/Snaps/SnapSettings';
import { CAN_INSTALL_THIRD_PARTY_SNAPS } from '../../../constants/snaps';
///: END:ONLY_INCLUDE_IF
import Routes from '../../../constants/navigation/Routes';
@@ -168,6 +171,7 @@ import BenefitFullView from '../../UI/Rewards/Views/BenefitFullView';
import BenefitsFullView from '../../UI/Rewards/Views/BenefitsFullView';
import { getDeFiProtocolPositionDetailsNavbarOptions } from '../../UI/Navbar';
import MoneyTabPressTracker from '../../UI/Money/components/MoneyTabPressTracker';
+import { withMessenger } from '../../../messengers/helpers/route-messenger-helpers';
const Stack = createStackNavigator();
const NativeStack = createNativeStackNavigator();
@@ -430,6 +434,10 @@ const ExploreHome = () => {
};
///: BEGIN:ONLY_INCLUDE_IF(snaps)
+const SnapSettingsWithMessenger = withMessenger(SnapSettings, {
+ capabilities: SNAPS_SETTINGS_ROUTE_ALLOWED_CAPABILITIES,
+});
+
const SnapsSettingsStack = () => {
const { colors } = useTheme();
return (
@@ -445,7 +453,7 @@ const SnapsSettingsStack = () => {
/>
);
diff --git a/app/components/UI/Bridge/hooks/useSubmitBatchSellTx/index.ts b/app/components/UI/Bridge/hooks/useSubmitBatchSellTx/index.ts
index c93eca484de..5582db5a92b 100644
--- a/app/components/UI/Bridge/hooks/useSubmitBatchSellTx/index.ts
+++ b/app/components/UI/Bridge/hooks/useSubmitBatchSellTx/index.ts
@@ -4,6 +4,7 @@ import type {
QuoteMetadata,
QuoteResponse,
} from '@metamask/bridge-controller';
+import type { BridgeStatusController } from '@metamask/bridge-status-controller';
import Engine from '../../../../../core/Engine';
import { selectBatchSellDestToken } from '../../../../../core/redux/slices/bridge';
@@ -37,8 +38,14 @@ export function useSubmitBatchSellTx() {
: quoteResponse,
);
+ // Type assertion needed: QuoteResponse/QuoteMetadata are imported from
+ // @metamask/bridge-controller v74 but submitBatchSell expects types from
+ // the v75 copy nested in @metamask/bridge-status-controller (FeatureId
+ // enum is structurally identical but nominally incompatible).
return await Engine.context.BridgeStatusController.submitBatchSell({
- quoteResponses: normalizedQuoteResponses,
+ quoteResponses: normalizedQuoteResponses as Parameters<
+ BridgeStatusController['submitBatchSell']
+ >[0]['quoteResponses'],
accountAddress: walletAddress,
location,
isStxEnabled: stxEnabled,
diff --git a/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx b/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx
index 34bcfa2697a..3d15475cc93 100644
--- a/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx
+++ b/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx
@@ -118,6 +118,7 @@ jest.mock('../../../../../core/NavigationService', () => ({
}));
jest.mock('../../utils/moneyFormatFiat', () => ({
+ ...jest.requireActual('../../utils/moneyFormatFiat'),
moneyFormatFiat: jest.fn(() => '$0.12'),
}));
@@ -584,7 +585,126 @@ describe('MoneyHomeView', () => {
});
});
- it('MoneyHowItWorks stays mounted in noAccount state when balance is unfunded', () => {
+ describe('dust threshold gating', () => {
+ const dustMock = {
+ totalFiatFormatted: '$0.00',
+ musdFiatFormatted: '$0.00',
+ musdSHFvdFiatFormatted: '$0.00',
+ // 0.0001 < DUST_THRESHOLD (0.01) — displays as $0.00 but is above zero
+ totalFiatRaw: '0.0001',
+ withdrawableMusd: undefined,
+ isAggregatedBalanceLoading: false,
+ isBalanceFetchError: false,
+ isBalanceFetching: false,
+ refetchBalance: jest.fn(),
+ apyDecimal: 0.05,
+ apyPercent: 5,
+ apyPercentFormatted: '5%',
+ vaultApyQuery: { data: { apy: 0.05 }, isLoading: false },
+ musdBalanceQuery: { data: undefined, isLoading: false },
+ musdEquivalentBalanceQuery: { data: undefined, isLoading: false },
+ } as unknown as ReturnType;
+
+ beforeEach(() => {
+ mockUseMoneyAccountBalance.mockReturnValue(dustMock);
+ mockUseMoneyAccountTransactions.mockReturnValue({
+ allTransactions: [],
+ deposits: [],
+ transfers: [],
+ submittedTransactions: [],
+ moneyAddress: '0x0000000000000000000000000000000000000001',
+ mockDataEnabled: false,
+ });
+ });
+
+ it('disables Transfer button for sub-cent (dust) balance', () => {
+ const { getByTestId } = renderWithProvider();
+ const transferButton = getByTestId(
+ MoneyActionButtonRowTestIds.TRANSFER_BUTTON,
+ );
+ expect(transferButton.props.accessibilityState?.disabled).toBe(true);
+ });
+
+ it('hides MoneyEarnings for dust balance', () => {
+ const { queryByTestId } = renderWithProvider();
+ expect(
+ queryByTestId(MoneyEarningsTestIds.CONTAINER),
+ ).not.toBeOnTheScreen();
+ });
+
+ it('shows unfunded onboarding sections for dust balance', () => {
+ const { getByTestId } = renderWithProvider();
+ expect(getByTestId(MoneyHowItWorksTestIds.CONTAINER)).toBeOnTheScreen();
+ expect(getByTestId(MoneyWhatYouGetTestIds.CONTAINER)).toBeOnTheScreen();
+ });
+ });
+
+ describe('missing-rate (unavailable) state', () => {
+ beforeEach(() => {
+ mockUseMoneyAccountBalance.mockReturnValue({
+ totalFiatFormatted: undefined,
+ totalFiatRaw: undefined,
+ isAggregatedBalanceLoading: false,
+ isBalanceFetchError: false,
+ isBalanceFetching: false,
+ refetchBalance: jest.fn(),
+ apyPercent: 5,
+ vaultApyQuery: { data: { apy: 0.05 }, isLoading: false },
+ musdBalanceQuery: { data: undefined, isLoading: false },
+ musdEquivalentBalanceQuery: { data: undefined, isLoading: false },
+ } as unknown as ReturnType);
+ mockUseMoneyAccountTransactions.mockReturnValue({
+ allTransactions: [],
+ deposits: [],
+ transfers: [],
+ submittedTransactions: [],
+ moneyAddress: '0x0000000000000000000000000000000000000001',
+ mockDataEnabled: false,
+ });
+ });
+
+ it('disables Transfer button', () => {
+ const { getByTestId } = renderWithProvider();
+ const transferButton = getByTestId(
+ MoneyActionButtonRowTestIds.TRANSFER_BUTTON,
+ );
+ expect(transferButton.props.accessibilityState?.disabled).toBe(true);
+ });
+
+ it('hides MoneyEarnings', () => {
+ const { queryByTestId } = renderWithProvider();
+ expect(
+ queryByTestId(MoneyEarningsTestIds.CONTAINER),
+ ).not.toBeOnTheScreen();
+ });
+
+ it('hides unfunded onboarding sections — does not fake an empty account', () => {
+ const { queryByTestId } = renderWithProvider();
+ expect(
+ queryByTestId(MoneyHowItWorksTestIds.CONTAINER),
+ ).not.toBeOnTheScreen();
+ expect(
+ queryByTestId(MoneyWhatYouGetTestIds.CONTAINER),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('spendable balance regression guard', () => {
+ it('enables Transfer button for a balance above the dust threshold', () => {
+ const { getByTestId } = renderWithProvider();
+ const transferButton = getByTestId(
+ MoneyActionButtonRowTestIds.TRANSFER_BUTTON,
+ );
+ expect(transferButton.props.accessibilityState?.disabled).toBeFalsy();
+ });
+
+ it('shows MoneyEarnings for a balance above the dust threshold', () => {
+ const { getByTestId } = renderWithProvider();
+ expect(getByTestId(MoneyEarningsTestIds.CONTAINER)).toBeOnTheScreen();
+ });
+ });
+
+ it('hides MoneyHowItWorks in noAccount state (displayState is noAccount, not balance)', () => {
mockUseMoneyAccountInfo.mockReturnValue({
hasMoneyAccount: false,
primaryMoneyAccount: undefined,
@@ -593,7 +713,6 @@ describe('MoneyHomeView', () => {
mockUseMoneyAccountBalance.mockReturnValue({
totalFiatFormatted: '$0.00',
totalFiatRaw: '0',
- tokenTotal: new BigNumber(0),
isAggregatedBalanceLoading: false,
isBalanceFetchError: false,
isBalanceFetching: false,
@@ -612,9 +731,12 @@ describe('MoneyHomeView', () => {
mockDataEnabled: false,
});
- const { getByTestId } = renderWithProvider();
+ const { queryByTestId } = renderWithProvider();
- expect(getByTestId(MoneyHowItWorksTestIds.CONTAINER)).toBeOnTheScreen();
+ // hasBalanceValue is false in noAccount state, so unfunded sections are hidden.
+ expect(
+ queryByTestId(MoneyHowItWorksTestIds.CONTAINER),
+ ).not.toBeOnTheScreen();
});
});
@@ -693,6 +815,62 @@ describe('MoneyHomeView', () => {
});
});
+ describe('transfer button disabled state', () => {
+ it('disables Transfer button when initial balance is loading', () => {
+ mockUseMoneyAccountBalance.mockReturnValue({
+ ...mockUseMoneyAccountBalance.mock.results[0]?.value,
+ tokenTotal: undefined,
+ isAggregatedBalanceLoading: true,
+ isBalanceFetchError: false,
+ isBalanceFetching: true,
+ } as unknown as ReturnType);
+
+ const { getByTestId } = renderWithProvider();
+ const transferButton = getByTestId(
+ MoneyActionButtonRowTestIds.TRANSFER_BUTTON,
+ );
+
+ expect(transferButton.props.accessibilityState?.disabled).toBe(true);
+ });
+
+ it('disables Transfer button when account has zero balance', () => {
+ mockUseMoneyAccountBalance.mockReturnValue({
+ ...mockUseMoneyAccountBalance.mock.results[0]?.value,
+ totalFiatFormatted: '$0.00',
+ totalFiatRaw: '0',
+ isAggregatedBalanceLoading: false,
+ isBalanceFetchError: false,
+ isBalanceFetching: false,
+ } as unknown as ReturnType);
+
+ const { getByTestId } = renderWithProvider();
+ const transferButton = getByTestId(
+ MoneyActionButtonRowTestIds.TRANSFER_BUTTON,
+ );
+
+ expect(transferButton.props.accessibilityState?.disabled).toBe(true);
+ });
+
+ it('does not navigate to Transfer sheet when Transfer button is pressed while disabled', () => {
+ mockUseMoneyAccountBalance.mockReturnValue({
+ ...mockUseMoneyAccountBalance.mock.results[0]?.value,
+ totalFiatFormatted: '$0.00',
+ totalFiatRaw: '0',
+ isAggregatedBalanceLoading: false,
+ isBalanceFetchError: false,
+ isBalanceFetching: false,
+ } as unknown as ReturnType);
+
+ const { getByTestId } = renderWithProvider();
+
+ fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.TRANSFER_BUTTON));
+
+ expect(mockNavigate).not.toHaveBeenCalledWith(Routes.MONEY.MODALS.ROOT, {
+ screen: Routes.MONEY.MODALS.TRANSFER_MONEY_SHEET,
+ });
+ });
+ });
+
it('navigates to Card root when Card button is pressed', () => {
const { getByTestId } = renderWithProvider();
@@ -827,12 +1005,14 @@ describe('MoneyHomeView', () => {
it('drops the + prefix when projected earnings round to formatted zero', () => {
mockMoneyFormatFiat.mockReturnValue('$0.00');
+ // totalFiatRaw must be >= DUST_THRESHOLD so hasSpendableBalance is true
+ // and MoneyEarnings renders. moneyFormatFiat is mocked to return '$0.00'
+ // for all values, exercising the no-plus-prefix path.
mockUseMoneyAccountBalance.mockReturnValue({
- totalFiatFormatted: '$0.00',
- musdFiatFormatted: '$0.00',
- musdSHFvdFiatFormatted: '$0.00',
- totalFiatRaw: '0.001',
- tokenTotal: undefined,
+ totalFiatFormatted: '$3.00',
+ musdFiatFormatted: '$1.00',
+ musdSHFvdFiatFormatted: '$2.00',
+ totalFiatRaw: '3',
isAggregatedBalanceLoading: false,
apyDecimal: 0.05,
apyPercent: 5,
@@ -1212,13 +1392,12 @@ describe('MoneyHomeView', () => {
});
});
- describe('balance not ready (tokenTotal undefined)', () => {
+ describe('balance not ready (loading state)', () => {
beforeEach(() => {
mockUseMoneyAccountBalance.mockReturnValue({
- totalFiatFormatted: '$3.00',
- totalFiatRaw: '3',
- tokenTotal: undefined,
- isAggregatedBalanceLoading: false,
+ totalFiatFormatted: undefined,
+ totalFiatRaw: undefined,
+ isAggregatedBalanceLoading: true,
isBalanceFetchError: false,
isBalanceFetching: false,
refetchBalance: mockRefetchBalance,
diff --git a/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx b/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx
index 252ddea71c0..6e646425bb0 100644
--- a/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx
+++ b/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx
@@ -31,8 +31,7 @@ import MoneyActivityLoading from '../../components/MoneyActivityLoading/MoneyAct
import useMoneyAccountBalance from '../../hooks/useMoneyAccountBalance';
import useMoneyAccountInfo from '../../hooks/useMoneyAccountInfo';
import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
-import { moneyFormatFiat } from '../../utils/moneyFormatFiat';
-import { isAccountFunded } from '../../utils/isAccountFunded';
+import { moneyFormatFiat, DUST_THRESHOLD } from '../../utils/moneyFormatFiat';
import { calculateProjectedEarnings } from '../../utils/projections';
import { MUSD_MAINNET_ASSET_FOR_DETAILS } from '../../../../Views/Homepage/Sections/Cash/CashGetMusdEmptyState.constants';
import { TokenDetailsSource } from '../../../TokenDetails/constants/constants';
@@ -63,7 +62,6 @@ import {
MONEY_URLS,
MONEY_BUTTON_TYPES,
} from '../../constants/moneyEvents';
-import { strings } from '../../../../../../locales/i18n';
import { TransactionMeta } from '@metamask/transaction-controller';
const Divider = () => ;
@@ -92,7 +90,6 @@ const MoneyHomeView = () => {
const {
totalFiatFormatted,
totalFiatRaw,
- tokenTotal,
vaultApyQuery,
isAggregatedBalanceLoading,
isBalanceFetchError,
@@ -148,9 +145,6 @@ const MoneyHomeView = () => {
isLinking,
} = useMoneyAccountCardLinkage();
- const balanceReady = tokenTotal !== undefined;
- const isFunded = isAccountFunded(tokenTotal) || activityItems.length > 0;
-
let displayState: MoneyBalanceDisplayState;
if (!hasMoneyAccount) {
displayState = { kind: 'noAccount' };
@@ -166,6 +160,13 @@ const MoneyHomeView = () => {
displayState = { kind: 'balance', value: totalFiatFormatted };
}
+ const hasBalanceValue = displayState.kind === 'balance';
+ const hasSpendableBalance =
+ hasBalanceValue &&
+ !!totalFiatRaw &&
+ new BigNumber(totalFiatRaw).abs().gte(DUST_THRESHOLD);
+ const isFunded = hasSpendableBalance || activityItems.length > 0;
+
const formattedZero = useMemo(
() => moneyFormatFiat(new BigNumber(0), currentCurrency),
[currentCurrency],
@@ -540,35 +541,34 @@ const MoneyHomeView = () => {
onApyInfoPress={handleApyInfoPress}
/>
- handleAddPress({
- labelKey: 'money.action.add',
- componentName: COMPONENT_NAMES.MONEY_ACTION_BUTTON_ROW,
- buttonPosition: 1,
- buttonRowButtonCount: ACTION_BUTTON_ROW_BUTTON_COUNT,
- })
- }
- onTransferPress={handleTransferPress}
- onCardPress={handleActionButtonCardPress}
+ add={{
+ onPress: () =>
+ handleAddPress({
+ labelKey: 'money.action.add',
+ componentName: COMPONENT_NAMES.MONEY_ACTION_BUTTON_ROW,
+ buttonPosition: 1,
+ buttonRowButtonCount: ACTION_BUTTON_ROW_BUTTON_COUNT,
+ }),
+ }}
+ transfer={{
+ onPress: handleTransferPress,
+ disabled: !hasSpendableBalance,
+ }}
+ card={{ onPress: handleActionButtonCardPress }}
/>
- {/* Only show earnings if balance is available and non-zero */}
- {displayState.kind === 'balance' &&
- totalFiatRaw &&
- !new BigNumber(totalFiatRaw).isZero() && (
- <>
-
-
- >
- )}
- {balanceReady && !isFunded && (
+ {hasSpendableBalance && (
+ <>
+
+
+ >
+ )}
+ {hasBalanceValue && !isFunded && (
<>
{
>
)}
- {balanceReady && !isFunded && (
+ {hasBalanceValue && !isFunded && (
{
it('renders all three action buttons', () => {
- const { getByText } = render(
- ,
- );
+ const { getByText } = render();
expect(getByText(strings('money.action.add'))).toBeOnTheScreen();
expect(getByText(strings('money.action.transfer'))).toBeOnTheScreen();
expect(getByText(strings('money.action.card'))).toBeOnTheScreen();
});
- it('calls onAddPress when Add button is pressed', () => {
+ it('calls add.onPress when Add button is pressed', () => {
const mockAdd = jest.fn();
const { getByTestId } = render(
- ,
+ ,
);
fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.ADD_BUTTON));
@@ -36,13 +32,12 @@ describe('MoneyActionButtonRow', () => {
expect(mockAdd).toHaveBeenCalledTimes(1);
});
- it('calls onTransferPress when Transfer button is pressed', () => {
+ it('calls transfer.onPress when Transfer button is pressed', () => {
const mockTransfer = jest.fn();
const { getByTestId } = render(
,
);
@@ -51,18 +46,56 @@ describe('MoneyActionButtonRow', () => {
expect(mockTransfer).toHaveBeenCalledTimes(1);
});
- it('calls onCardPress when Card button is pressed', () => {
+ it('calls card.onPress when Card button is pressed', () => {
+ const mockCard = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+
+ fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.CARD_BUTTON));
+
+ expect(mockCard).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not call add.onPress when Add button is disabled', () => {
+ const mockAdd = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+
+ fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.ADD_BUTTON));
+
+ expect(mockAdd).not.toHaveBeenCalled();
+ });
+
+ it('does not call transfer.onPress when Transfer button is disabled', () => {
+ const mockTransfer = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+
+ fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.TRANSFER_BUTTON));
+
+ expect(mockTransfer).not.toHaveBeenCalled();
+ });
+
+ it('does not call card.onPress when Card button is disabled', () => {
const mockCard = jest.fn();
const { getByTestId } = render(
,
);
fireEvent.press(getByTestId(MoneyActionButtonRowTestIds.CARD_BUTTON));
- expect(mockCard).toHaveBeenCalledTimes(1);
+ expect(mockCard).not.toHaveBeenCalled();
});
});
diff --git a/app/components/UI/Money/components/MoneyActionButtonRow/MoneyActionButtonRow.tsx b/app/components/UI/Money/components/MoneyActionButtonRow/MoneyActionButtonRow.tsx
index e4b8da891f3..150c7820991 100644
--- a/app/components/UI/Money/components/MoneyActionButtonRow/MoneyActionButtonRow.tsx
+++ b/app/components/UI/Money/components/MoneyActionButtonRow/MoneyActionButtonRow.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React from 'react';
import {
Box,
BoxAlignItems,
@@ -17,10 +17,15 @@ import {
import { strings } from '../../../../../../locales/i18n';
import { MoneyActionButtonRowTestIds } from './MoneyActionButtonRow.testIds';
+interface ActionButtonConfig {
+ onPress: () => void;
+ disabled?: boolean;
+}
+
interface MoneyActionButtonRowProps {
- onAddPress: () => void;
- onTransferPress: () => void;
- onCardPress: () => void;
+ add: ActionButtonConfig;
+ transfer: ActionButtonConfig;
+ card: ActionButtonConfig;
}
const ActionButton = ({
@@ -28,16 +33,19 @@ const ActionButton = ({
label,
onPress,
testID,
+ disabled,
}: {
iconName: IconName;
label: string;
onPress: () => void;
testID: string;
+ disabled?: boolean;
}) => (
{
- const handleAddPress = useCallback(() => onAddPress(), [onAddPress]);
- const handleTransferPress = useCallback(
- () => onTransferPress(),
- [onTransferPress],
- );
- const handleCardPress = useCallback(() => onCardPress(), [onCardPress]);
-
- return (
-
-
-
-
-
- );
-};
+ add,
+ transfer,
+ card,
+}: MoneyActionButtonRowProps) => (
+
+
+
+
+
+);
export default MoneyActionButtonRow;
diff --git a/app/components/UI/Money/utils/isAccountFunded.test.ts b/app/components/UI/Money/utils/isAccountFunded.test.ts
deleted file mode 100644
index dc39b2cc5d2..00000000000
--- a/app/components/UI/Money/utils/isAccountFunded.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { BigNumber } from 'bignumber.js';
-import { isAccountFunded } from './isAccountFunded';
-
-describe('isAccountFunded', () => {
- it('returns false when tokenTotal is undefined', () => {
- expect(isAccountFunded(undefined)).toBe(false);
- });
-
- it('returns false when tokenTotal is zero', () => {
- expect(isAccountFunded(new BigNumber(0))).toBe(false);
- });
-
- it('returns true when tokenTotal is positive', () => {
- expect(isAccountFunded(new BigNumber('0.01'))).toBe(true);
- expect(isAccountFunded(new BigNumber(100))).toBe(true);
- });
-
- it('returns false when tokenTotal is negative', () => {
- expect(isAccountFunded(new BigNumber(-1))).toBe(false);
- });
-});
diff --git a/app/components/UI/Money/utils/isAccountFunded.ts b/app/components/UI/Money/utils/isAccountFunded.ts
deleted file mode 100644
index 5479db8539c..00000000000
--- a/app/components/UI/Money/utils/isAccountFunded.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type BigNumber from 'bignumber.js';
-
-export const isAccountFunded = (tokenTotal: BigNumber | undefined): boolean =>
- tokenTotal !== undefined && tokenTotal.gt(0);
diff --git a/app/components/UI/Money/utils/moneyFormatFiat.ts b/app/components/UI/Money/utils/moneyFormatFiat.ts
index ec087f1cece..ad444adf914 100644
--- a/app/components/UI/Money/utils/moneyFormatFiat.ts
+++ b/app/components/UI/Money/utils/moneyFormatFiat.ts
@@ -4,7 +4,7 @@ import { getLocaleLanguageCode } from '../../../hooks/useFormatters';
// One cent. Values strictly below this collapse to $0.00 — mUSD is USD-pegged
// so sub-cent fiat is economically meaningless.
-const THRESHOLD = 0.01;
+export const DUST_THRESHOLD = 0.01;
/**
* Helper that wraps formatWithThreshold to centralize Money formatting logic.
@@ -19,10 +19,10 @@ export const moneyFormatFiat = (
value: BigNumber,
currentCurrency: string,
): string => {
- const effective = value.abs().lt(THRESHOLD) ? new BigNumber(0) : value;
+ const effective = value.abs().lt(DUST_THRESHOLD) ? new BigNumber(0) : value;
return formatWithThreshold(
effective.toNumber(),
- THRESHOLD,
+ DUST_THRESHOLD,
getLocaleLanguageCode(),
{
style: 'currency',
diff --git a/app/components/UI/Predict/components/PredictGameChart/PredictGameChartContent.tsx b/app/components/UI/Predict/components/PredictGameChart/PredictGameChartContent.tsx
index d9e735bb812..465eeb4777d 100644
--- a/app/components/UI/Predict/components/PredictGameChart/PredictGameChartContent.tsx
+++ b/app/components/UI/Predict/components/PredictGameChart/PredictGameChartContent.tsx
@@ -168,7 +168,7 @@ const PredictGameChartContent: React.FC = ({
if (error) {
return (
-
+
= ({
if (!hasData) {
return (
-
+
No price history available
@@ -235,7 +235,7 @@ const PredictGameChartContent: React.FC = ({
const isTooltipActive = activeIndex >= 0;
return (
-
+
= ({
outcomeGroups: market.outcomeGroups ?? [],
});
- const { height: windowHeight } = useWindowDimensions();
- const scrollRef = useRef(null);
- const [stickyHeaderY, setStickyHeaderY] = useState(0);
- const pendingChipScroll = useRef(false);
-
- const handleStickyHeaderLayout = useCallback(
- (e: { nativeEvent: { layout: { y: number } } }) => {
- setStickyHeaderY(e.nativeEvent.layout.y);
- },
- [],
- );
-
- const onChipSelect = useCallback(
- (key: string) => {
- scrollRef.current?.scrollTo({
- y: stickyHeaderY,
- animated: false,
- });
- handleChipSelect(key);
- pendingChipScroll.current = true;
- },
- [handleChipSelect, stickyHeaderY],
- );
-
- // Guard: only scroll after an explicit chip selection (pendingChipScroll is
- // set to true in onChipSelect). This prevents scrolling on initial render
- // or when stickyHeaderY updates from layout measurements.
- useEffect(() => {
- if (!pendingChipScroll.current) return;
- pendingChipScroll.current = false;
- const handle = InteractionManager.runAfterInteractions(() => {
- scrollRef.current?.scrollTo({
- y: stickyHeaderY,
- animated: false,
- });
- });
- return () => handle.cancel();
- }, [activeChipKey, stickyHeaderY]);
-
const showStickyHeader = showTabBar || showChips;
const hasExtendedOutcomes = tabsEnabled && groupMap.size > 0;
const showFooter =
@@ -199,7 +148,6 @@ const PredictGameDetailsContent: React.FC = ({
= ({
{showStickyHeader && (
-
+
{showTabBar && (
= ({
)}
)}
-
-
-
+
{showFooter && (
diff --git a/app/components/UI/Predict/components/PredictSearchOverlay/PredictSearchOverlay.tsx b/app/components/UI/Predict/components/PredictSearchOverlay/PredictSearchOverlay.tsx
index 7fcc2964389..2266b8365a2 100644
--- a/app/components/UI/Predict/components/PredictSearchOverlay/PredictSearchOverlay.tsx
+++ b/app/components/UI/Predict/components/PredictSearchOverlay/PredictSearchOverlay.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
@@ -19,6 +19,7 @@ import PredictMarket from '../PredictMarket';
import PredictMarketSkeleton from '../PredictMarketSkeleton';
import PredictOffline from '../PredictOffline';
import { strings } from '../../../../../../locales/i18n';
+import Engine from '../../../../../core/Engine';
import { useTheme } from '../../../../../util/theme';
import HeaderSearch, {
HeaderSearchVariant,
@@ -36,6 +37,10 @@ export interface PredictSearchOverlayProps {
onSearchChange: (query: string) => void;
onClose: () => void;
transactionActiveAbTests?: TransactionActiveAbTestEntry[];
+ /** Active feed tab when search was opened (omitted on the tab-less home). */
+ predictFeedTab?: string;
+ /** How the user entered the surface where search was opened. */
+ entryPoint?: string;
}
const SEARCH_DEBOUNCE_MS = 200;
@@ -53,6 +58,8 @@ const PredictSearchOverlay: React.FC = ({
onSearchChange,
onClose,
transactionActiveAbTests,
+ predictFeedTab,
+ entryPoint,
}) => {
const tw = useTailwind();
const { colors } = useTheme();
@@ -66,28 +73,85 @@ const PredictSearchOverlay: React.FC = ({
const upDownEnabled = useSelector(selectPredictUpDownEnabledFlag);
const refine = upDownEnabled ? deduplicateSeriesMarkets : undefined;
- const { marketData, isFetching, error, refetch } = usePredictSearchMarketData(
- {
+ const { marketData, totalResults, isFetching, error, refetch } =
+ usePredictSearchMarketData({
q: debouncedSearchQuery,
pageSize: 20,
refine,
enabled: isVisible,
- },
- );
+ });
const isSearchLoading = isDebouncing || isFetching;
const hasSearchQuery = debouncedSearchQuery.trim().length > 0;
+ // Fire `queried` once per resolved (non-empty) query — i.e. after debounce
+ // settles and the fetch completes without error. The ref guards against
+ // re-firing for the same query when `marketData` re-renders, but clearing the
+ // live input resets the guard immediately so the same term can be tracked if
+ // the user searches again without closing the overlay.
+ const lastTrackedQueryRef = useRef(null);
+ useEffect(() => {
+ if (!searchQuery.trim()) {
+ lastTrackedQueryRef.current = null;
+ }
+ }, [searchQuery]);
+
+ useEffect(() => {
+ if (!isVisible) {
+ lastTrackedQueryRef.current = null;
+ return;
+ }
+ const q = debouncedSearchQuery.trim();
+ if (!q || isSearchLoading || error || lastTrackedQueryRef.current === q) {
+ return;
+ }
+ lastTrackedQueryRef.current = q;
+ Engine.context.PredictController.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.QUERIED,
+ predictFeedTab,
+ entryPoint,
+ searchQuery: q,
+ // Report the server's total match count, not the visible page length.
+ // `marketData` is capped at `pageSize` and post-dedup, so it undercounts
+ // any query matching more results than the first page holds.
+ resultsCount: totalResults,
+ });
+ }, [
+ isVisible,
+ debouncedSearchQuery,
+ isSearchLoading,
+ error,
+ totalResults,
+ predictFeedTab,
+ entryPoint,
+ ]);
+
+ const handleResultPress = useCallback(
+ (market: PredictMarketType) => {
+ Engine.context.PredictController.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.RESULT_CLICKED,
+ predictFeedTab,
+ entryPoint,
+ searchQuery: debouncedSearchQuery.trim(),
+ marketId: market.id,
+ marketTitle: market.title,
+ });
+ },
+ [predictFeedTab, entryPoint, debouncedSearchQuery],
+ );
+
const renderItem = useCallback(
(info: { item: PredictMarketType; index: number }) => (
handleResultPress(info.item)}
/>
),
- [transactionActiveAbTests],
+ [predictFeedTab, transactionActiveAbTests, handleResultPress],
);
const keyExtractor = useCallback((item: PredictMarketType) => item.id, []);
diff --git a/app/components/UI/Predict/constants/eventNames.ts b/app/components/UI/Predict/constants/eventNames.ts
index 384fc208956..ee858f491b2 100644
--- a/app/components/UI/Predict/constants/eventNames.ts
+++ b/app/components/UI/Predict/constants/eventNames.ts
@@ -90,6 +90,11 @@ export const PredictEventProperties = {
// Banner properties
BANNER_TYPE: 'banner_type',
+
+ // Search engagement
+ INTERACTION_TYPE: 'interaction_type',
+ SEARCH_QUERY: 'search_query',
+ RESULTS_COUNT: 'results_count',
} as const;
/**
@@ -163,6 +168,11 @@ export const PredictEventValues = {
BANNER_TYPE: {
WORLD_CUP: 'world_cup',
},
+ SEARCH_INTERACTION: {
+ OPENED: 'opened',
+ QUERIED: 'queried',
+ RESULT_CLICKED: 'result_clicked',
+ },
} as const;
/**
diff --git a/app/components/UI/Predict/controllers/PredictAnalytics.test.ts b/app/components/UI/Predict/controllers/PredictAnalytics.test.ts
index 8ad01115cc6..b3fd0b4b618 100644
--- a/app/components/UI/Predict/controllers/PredictAnalytics.test.ts
+++ b/app/components/UI/Predict/controllers/PredictAnalytics.test.ts
@@ -787,5 +787,101 @@ describe('PredictAnalytics', () => {
market_slug: 'slug-10',
});
});
+
+ it('tracks search opened with only the base properties', () => {
+ predictAnalytics.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.OPENED,
+ predictFeedTab: 'trending',
+ entryPoint: 'predict_feed',
+ });
+
+ const event = getTrackedEvent();
+
+ expect(event.name).toBe(
+ MetaMetricsEvents.PREDICT_SEARCH_INTERACTED.category,
+ );
+ expect(event.properties).toEqual({
+ interaction_type: 'opened',
+ predict_feed_tab: 'trending',
+ entry_point: 'predict_feed',
+ });
+ });
+
+ it('tracks search queried with search query and results count', () => {
+ predictAnalytics.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.QUERIED,
+ predictFeedTab: 'crypto',
+ entryPoint: 'predict_feed',
+ searchQuery: 'eth',
+ resultsCount: 5,
+ });
+
+ const event = getTrackedEvent();
+
+ expect(event.name).toBe(
+ MetaMetricsEvents.PREDICT_SEARCH_INTERACTED.category,
+ );
+ expect(event.properties).toMatchObject({
+ interaction_type: 'queried',
+ predict_feed_tab: 'crypto',
+ entry_point: 'predict_feed',
+ search_query: 'eth',
+ results_count: 5,
+ });
+ });
+
+ it('emits results_count of zero (does not drop the property)', () => {
+ predictAnalytics.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.QUERIED,
+ searchQuery: 'nomatch',
+ resultsCount: 0,
+ });
+
+ const event = getTrackedEvent();
+
+ expect(event.properties).toMatchObject({
+ search_query: 'nomatch',
+ results_count: 0,
+ });
+ });
+
+ it('tracks search result_clicked with market id and title', () => {
+ predictAnalytics.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.RESULT_CLICKED,
+ predictFeedTab: 'sports',
+ entryPoint: 'predict_feed',
+ searchQuery: 'cup',
+ marketId: 'm42',
+ marketTitle: 'World Cup Winner',
+ });
+
+ const event = getTrackedEvent();
+
+ expect(event.name).toBe(
+ MetaMetricsEvents.PREDICT_SEARCH_INTERACTED.category,
+ );
+ expect(event.properties).toMatchObject({
+ interaction_type: 'result_clicked',
+ predict_feed_tab: 'sports',
+ entry_point: 'predict_feed',
+ search_query: 'cup',
+ market_id: 'm42',
+ market_title: 'World Cup Winner',
+ });
+ });
+
+ it('omits optional properties (incl. predict_feed_tab) when not provided', () => {
+ predictAnalytics.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.OPENED,
+ entryPoint: 'home_section',
+ });
+
+ const event = getTrackedEvent();
+
+ expect(event.properties).toEqual({
+ interaction_type: 'opened',
+ entry_point: 'home_section',
+ });
+ });
});
});
diff --git a/app/components/UI/Predict/controllers/PredictAnalytics.ts b/app/components/UI/Predict/controllers/PredictAnalytics.ts
index b6bff98aed1..a6257b8b72a 100644
--- a/app/components/UI/Predict/controllers/PredictAnalytics.ts
+++ b/app/components/UI/Predict/controllers/PredictAnalytics.ts
@@ -145,6 +145,16 @@ export interface ShareActionArgs {
marketSlug?: string;
}
+export interface SearchInteractedArgs {
+ interactionType: string;
+ predictFeedTab?: string;
+ entryPoint?: string;
+ searchQuery?: string;
+ resultsCount?: number;
+ marketId?: string;
+ marketTitle?: string;
+}
+
const PREDICT_PORTFOLIO_MODULE_COMPONENT =
PredictEventValues.PREDICT_COMPONENT.PREDICT_PORTFOLIO_MODULE;
@@ -453,6 +463,10 @@ export class PredictAnalytics {
this.trackConfiguredEvent('shareAction', params);
}
+ public trackSearchInteracted(params: SearchInteractedArgs): void {
+ this.trackConfiguredEvent('searchInteracted', params);
+ }
+
private trackConfiguredEvent(
configKey: keyof typeof PREDICT_ANALYTICS_EVENTS,
args: object,
diff --git a/app/components/UI/Predict/controllers/PredictController-method-action-types.ts b/app/components/UI/Predict/controllers/PredictController-method-action-types.ts
index af4be03bef9..5c8fedfbd9c 100644
--- a/app/components/UI/Predict/controllers/PredictController-method-action-types.ts
+++ b/app/components/UI/Predict/controllers/PredictController-method-action-types.ts
@@ -139,6 +139,16 @@ export type PredictControllerTrackShareActionAction = {
handler: PredictController['trackShareAction'];
};
+/**
+ * Track Predict Search Interacted analytics event
+ *
+ * @public
+ */
+export type PredictControllerTrackSearchInteractedAction = {
+ type: `PredictController:trackSearchInteracted`;
+ handler: PredictController['trackSearchInteracted'];
+};
+
/**
* Track Predict Betslip Dismissed analytics event
*
@@ -356,6 +366,7 @@ export type PredictControllerMethodActions =
| PredictControllerTrackFeedViewedAction
| PredictControllerTrackBannerActionAction
| PredictControllerTrackShareActionAction
+ | PredictControllerTrackSearchInteractedAction
| PredictControllerTrackBetslipDismissedAction
| PredictControllerPreviewOrderAction
| PredictControllerPlaceOrderAction
diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts
index b179ff26ca3..1834ea83ce8 100644
--- a/app/components/UI/Predict/controllers/PredictController.ts
+++ b/app/components/UI/Predict/controllers/PredictController.ts
@@ -413,6 +413,7 @@ const MESSENGER_EXPOSED_METHODS = [
'trackPositionsScreenViewed',
'trackPositionsTabViewed',
'trackPredictOrderEvent',
+ 'trackSearchInteracted',
'trackShareAction',
] as const;
@@ -1273,6 +1274,17 @@ export class PredictController extends BaseController<
this.analytics.trackShareAction(args);
}
+ /**
+ * Track Predict Search Interacted analytics event
+ *
+ * @public
+ */
+ public trackSearchInteracted(
+ args: Parameters[0],
+ ): void {
+ this.analytics.trackSearchInteracted(args);
+ }
+
/**
* Track Predict Betslip Dismissed analytics event
*
@@ -3376,5 +3388,6 @@ export type {
PredictControllerTrackMarketDetailsOpenedAction,
PredictControllerTrackPositionViewedAction,
PredictControllerTrackPredictOrderEventAction,
+ PredictControllerTrackSearchInteractedAction,
PredictControllerTrackShareActionAction,
} from './PredictController-method-action-types';
diff --git a/app/components/UI/Predict/controllers/utils/predictAnalyticsEvents.ts b/app/components/UI/Predict/controllers/utils/predictAnalyticsEvents.ts
index 11784f9c0cd..ebd656c0ea3 100644
--- a/app/components/UI/Predict/controllers/utils/predictAnalyticsEvents.ts
+++ b/app/components/UI/Predict/controllers/utils/predictAnalyticsEvents.ts
@@ -17,7 +17,8 @@ export type PredictAnalyticsEventKey =
| 'shareAction'
| 'geoBlockTriggered'
| 'marketDetailsOpened'
- | 'bannerAction';
+ | 'bannerAction'
+ | 'searchInteracted';
const mapPortfolioProperties = ({
actionType,
@@ -198,4 +199,35 @@ export const PREDICT_ANALYTICS_EVENTS: Record<
: {}),
}),
},
+ searchInteracted: {
+ event: MetaMetricsEvents.PREDICT_SEARCH_INTERACTED,
+ logLabel: '📊 [Analytics] PREDICT_SEARCH_INTERACTED',
+ mapProperties: ({
+ interactionType,
+ predictFeedTab,
+ entryPoint,
+ searchQuery,
+ resultsCount,
+ marketId,
+ marketTitle,
+ }) => ({
+ [PredictEventProperties.INTERACTION_TYPE]: interactionType,
+ ...(predictFeedTab
+ ? { [PredictEventProperties.PREDICT_FEED_TAB]: predictFeedTab }
+ : {}),
+ ...(entryPoint
+ ? { [PredictEventProperties.ENTRY_POINT]: entryPoint }
+ : {}),
+ ...(searchQuery !== undefined
+ ? { [PredictEventProperties.SEARCH_QUERY]: searchQuery }
+ : {}),
+ ...(resultsCount !== undefined
+ ? { [PredictEventProperties.RESULTS_COUNT]: resultsCount }
+ : {}),
+ ...(marketId ? { [PredictEventProperties.MARKET_ID]: marketId } : {}),
+ ...(marketTitle
+ ? { [PredictEventProperties.MARKET_TITLE]: marketTitle }
+ : {}),
+ }),
+ },
};
diff --git a/app/components/UI/Predict/routes/index.test.tsx b/app/components/UI/Predict/routes/index.test.tsx
index f3e30a2e711..6378480ef71 100644
--- a/app/components/UI/Predict/routes/index.test.tsx
+++ b/app/components/UI/Predict/routes/index.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render, screen, act, waitFor } from '@testing-library/react-native';
+import { render, screen, act } from '@testing-library/react-native';
import {
NavigationContainer,
NavigationContainerRef,
@@ -207,7 +207,7 @@ describe('PredictScreenStack', () => {
expect(screen.getByTestId('predict-positions-view')).toBeOnTheScreen();
});
- it('returns to market list when POSITIONS screen is disabled', async () => {
+ it('navigates to POSITIONS screen when portfolio flag is disabled', async () => {
mockPredictPortfolioEnabled = false;
renderWithNavigation();
@@ -215,12 +215,7 @@ describe('PredictScreenStack', () => {
navigationRef.current?.navigate(Routes.PREDICT.POSITIONS);
});
- await waitFor(() => {
- expect(navigationRef.current?.getCurrentRoute()?.name).toBe(
- Routes.PREDICT.MARKET_LIST,
- );
- });
- expect(screen.queryByTestId('predict-positions-view')).toBeNull();
+ expect(screen.getByTestId('predict-positions-view')).toBeOnTheScreen();
});
it('navigates to BUY_PREVIEW with PredictBuyPreview when payWithAnyToken is off', async () => {
diff --git a/app/components/UI/Predict/routes/index.tsx b/app/components/UI/Predict/routes/index.tsx
index a097693e88c..cead671f0a1 100644
--- a/app/components/UI/Predict/routes/index.tsx
+++ b/app/components/UI/Predict/routes/index.tsx
@@ -1,6 +1,5 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import { NavigationProp, useNavigation } from '@react-navigation/native';
-import React, { useEffect } from 'react';
+import React from 'react';
import { strings } from '../../../../../locales/i18n';
import Routes from '../../../../constants/navigation/Routes';
import {
@@ -25,41 +24,11 @@ import { PredictPreviewSheetProvider } from '../contexts';
import PredictBuyPreview from '../views/PredictBuyPreview/PredictBuyPreview';
import PredictBuyWithAnyToken from '../views/PredictBuyWithAnyToken';
import PredictSellPreview from '../views/PredictSellPreview/PredictSellPreview';
-import {
- selectPredictPortfolioEnabledFlag,
- selectPredictWithAnyTokenEnabledFlag,
-} from '../selectors/featureFlags';
+import { selectPredictWithAnyTokenEnabledFlag } from '../selectors/featureFlags';
const Stack = createNativeStackNavigator();
const ModalStack = createNativeStackNavigator();
-const PredictPositionsRoute = () => {
- const navigation =
- useNavigation>();
- const predictPortfolioEnabled = useSelector(
- selectPredictPortfolioEnabledFlag,
- );
-
- useEffect(() => {
- if (predictPortfolioEnabled) {
- return;
- }
-
- if (navigation.canGoBack()) {
- navigation.goBack();
- return;
- }
-
- navigation.navigate(Routes.PREDICT.MARKET_LIST);
- }, [navigation, predictPortfolioEnabled]);
-
- if (!predictPortfolioEnabled) {
- return null;
- }
-
- return ;
-};
-
const PredictModalStack = () => {
const emptyNavHeaderOptions = useEmptyNavHeaderForConfirmations();
@@ -131,7 +100,7 @@ const PredictScreenStack = () => {
{
});
jest.mock(
- '../../../../../../../component-library/components/Skeleton/Skeleton',
+ '../../../../../../../component-library/components-temp/Skeleton/Skeleton',
() => {
const { View: RNView } = jest.requireActual('react-native');
return function MockSkeleton(props: Record) {
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.tsx
index 9e05d3b6ad1..1f38638fc07 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.tsx
@@ -11,7 +11,7 @@ import { useTailwind } from '@metamask/design-system-twrnc-preset';
import React, { useEffect, useRef } from 'react';
import { Animated } from 'react-native';
import { strings } from '../../../../../../../../locales/i18n';
-import Skeleton from '../../../../../../../component-library/components/Skeleton/Skeleton';
+import Skeleton from '../../../../../../../component-library/components-temp/Skeleton/Skeleton';
import { formatPrice } from '../../../../utils/format';
import PredictAmountDisplay from '../../../../components/PredictAmountDisplay';
import type { PredictKeypadHandles } from '../../../../components/PredictKeypad';
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictFeeSummary/PredictFeeSummary.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictFeeSummary/PredictFeeSummary.tsx
index d50b6bc4824..ae134843752 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictFeeSummary/PredictFeeSummary.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictFeeSummary/PredictFeeSummary.tsx
@@ -19,7 +19,7 @@ import { strings } from '../../../../../../../../locales/i18n';
import KeyValueRow from '../../../../../../../component-library/components-temp/KeyValueRow';
import { TooltipSizes } from '../../../../../../../component-library/components-temp/KeyValueRow/KeyValueRow.types';
import { IconName as IconNameLegacy } from '../../../../../../../component-library/components/Icons/Icon';
-import Skeleton from '../../../../../../../component-library/components/Skeleton/Skeleton';
+import Skeleton from '../../../../../../../component-library/components-temp/Skeleton/Skeleton';
import {
TextColor as LegacyTextColor,
TextVariant as LegacyTextVariant,
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
index 71ea76b0189..1eafea8b957 100644
--- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx
@@ -21,6 +21,21 @@ jest.mock('react-native-reanimated', () => {
return Reanimated;
});
+const mockTrackSearchInteracted = jest.fn();
+
+jest.mock('../../../../../core/Engine', () => ({
+ __esModule: true,
+ default: {
+ context: {
+ PredictController: {
+ trackSearchInteracted: (
+ ...args: Parameters
+ ) => mockTrackSearchInteracted(...args),
+ },
+ },
+ },
+}));
+
import PredictFeed from './PredictFeed';
import { PredictBalance } from '../../components/PredictBalance';
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
index 04dfac120cb..72836c74478 100644
--- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx
@@ -79,6 +79,7 @@ import { strings } from '../../../../../../locales/i18n';
import { useTheme } from '../../../../../util/theme';
import { TraceName } from '../../../../../util/trace';
import Routes from '../../../../../constants/navigation/Routes';
+import Engine from '../../../../../core/Engine';
import {
TabItem,
TabsBar,
@@ -658,6 +659,15 @@ const PredictFeed: React.FC = ({
withdrawUnavailableSheetRef.current?.onOpenBottomSheet();
}, []);
+ const handleShowSearch = useCallback(() => {
+ Engine.context.PredictController.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.OPENED,
+ predictFeedTab: tabs[activeIndex]?.key,
+ entryPoint: listEntryPoint,
+ });
+ showSearch();
+ }, [tabs, activeIndex, listEntryPoint, showSearch]);
+
const headerTopInset = hideHeader ? topInset : 0;
return (
@@ -685,7 +695,7 @@ const PredictFeed: React.FC = ({
endButtonIconProps={[
{
iconName: IconName.Search,
- onPress: showSearch,
+ onPress: handleShowSearch,
testID: PredictSearchSelectorsIDs.SEARCH_BUTTON,
},
]}
@@ -731,6 +741,8 @@ const PredictFeed: React.FC = ({
onSearchChange={setSearchQuery}
onClose={clearSearchAndClose}
transactionActiveAbTests={transactionActiveAbTests}
+ predictFeedTab={tabs[activeIndex]?.key}
+ entryPoint={listEntryPoint}
/>
diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
index d49a55fde11..93e0e48690a 100644
--- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
+++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.view.test.tsx
@@ -7,8 +7,10 @@
*
* Run with: yarn jest -c jest.config.view.js PredictFeed.view.test --runInBand --silent --coverage=false
*/
+import React from 'react';
import '../../../../../../tests/component-view/mocks';
import Engine from '../../../../../../app/core/Engine';
+import { useRoute } from '@react-navigation/native';
import {
renderPredictFeedView,
renderPredictFeedViewWithRoutes,
@@ -19,6 +21,7 @@ import {
waitFor,
within,
} from '@testing-library/react-native';
+import { Text } from 'react-native';
import {
PredictMarketListSelectorsIDs,
PredictSearchSelectorsIDs,
@@ -97,6 +100,33 @@ const SEARCH_PLACEHOLDER = 'Search prediction markets';
const CANCEL_TEXT = 'Cancel';
const RETRY_TEXT = 'Retry';
+interface PredictRootRouteParams {
+ screen?: string;
+ params?: {
+ predictFeedTab?: string;
+ };
+}
+
+const PredictRootRouteParamsProbe = () => {
+ const route = useRoute<{
+ key: string;
+ name: string;
+ params?: PredictRootRouteParams;
+ }>();
+ const predictFeedTab = route.params?.params?.predictFeedTab ?? 'missing';
+
+ return (
+ <>
+
+ {route.params?.screen ?? 'unknown'}
+
+
+ {predictFeedTab}
+
+ >
+ );
+};
+
const createPredictMarket = ({
id,
category,
@@ -199,6 +229,22 @@ describe('PredictFeed', () => {
expect(await findByPlaceholderText(SEARCH_PLACEHOLDER)).toBeOnTheScreen();
});
+ it('tracks the search opened event with the active feed tab and entry point', async () => {
+ const { getByTestId } = renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ expect(
+ Engine.context.PredictController.trackSearchInteracted,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ interactionType: 'opened',
+ predictFeedTab: expect.any(String),
+ entryPoint: 'predict_feed',
+ }),
+ );
+ });
+
it('calls PredictController.searchMarkets with the typed query after the user searches', async () => {
const searchMarketsSpy = jest.spyOn(
Engine.context.PredictController,
@@ -317,6 +363,99 @@ describe('PredictFeed', () => {
expect(within(resultCard).getByText(/Yes/)).toBeOnTheScreen();
expect(within(resultCard).getByText(/No/)).toBeOnTheScreen();
+ // Assert — the `queried` event fires once with the resolved results count
+ await waitFor(() => {
+ expect(
+ Engine.context.PredictController.trackSearchInteracted,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ interactionType: 'queried',
+ searchQuery: 'bitcoin',
+ resultsCount: 1,
+ }),
+ );
+ });
+
+ searchMarketsSpy.mockRestore();
+ });
+
+ it('reports the server total match count in the queried event, not the visible page length', async () => {
+ // Arrange — one market on the first page, but 42 matches server-side.
+ // `marketData.length` (1) must not be used as the result count.
+ const searchMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'searchMarkets',
+ );
+ searchMarketsSpy.mockResolvedValue({
+ markets: [MOCK_PREDICT_MARKET],
+ totalResults: 42,
+ });
+ const { getByTestId, findByPlaceholderText } = renderPredictFeedView();
+
+ // Act — user opens search and types a query
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ // Assert — resultsCount is the server total (42), not the page length (1)
+ await waitFor(() => {
+ expect(
+ Engine.context.PredictController.trackSearchInteracted,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ interactionType: 'queried',
+ searchQuery: 'bitcoin',
+ resultsCount: 42,
+ }),
+ );
+ });
+
+ searchMarketsSpy.mockRestore();
+ });
+
+ it('tracks the same query again after the user clears and retypes it', async () => {
+ const searchMarketsSpy = jest.spyOn(
+ Engine.context.PredictController,
+ 'searchMarkets',
+ );
+ searchMarketsSpy.mockResolvedValue({
+ markets: [MOCK_PREDICT_MARKET],
+ totalResults: 1,
+ });
+
+ const trackSearchInteractedMock = Engine.context.PredictController
+ .trackSearchInteracted as jest.Mock;
+ const { getByTestId, findByPlaceholderText } = renderPredictFeedView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+ trackSearchInteractedMock.mockClear();
+
+ const searchInput = await findByPlaceholderText(SEARCH_PLACEHOLDER);
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ const getQueriedEvents = () =>
+ trackSearchInteractedMock.mock.calls
+ .map(([args]) => args as { interactionType?: string })
+ .filter((args) => args.interactionType === 'queried');
+
+ await waitFor(() => {
+ expect(getQueriedEvents()).toHaveLength(1);
+ });
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.CLEAR_BUTTON));
+
+ await waitFor(() => {
+ expect(searchMarketsSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ q: '' }),
+ );
+ });
+
+ fireEvent.changeText(searchInput, 'bitcoin');
+
+ await waitFor(() => {
+ expect(getQueriedEvents()).toHaveLength(2);
+ });
+
searchMarketsSpy.mockRestore();
});
@@ -332,7 +471,12 @@ describe('PredictFeed', () => {
const { getByTestId, findByPlaceholderText, findByTestId } =
renderPredictFeedViewWithRoutes({
- extraRoutes: [{ name: Routes.PREDICT.ROOT }],
+ extraRoutes: [
+ {
+ name: Routes.PREDICT.ROOT,
+ Component: PredictRootRouteParamsProbe,
+ },
+ ],
});
fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
@@ -350,6 +494,19 @@ describe('PredictFeed', () => {
expect(
await findByTestId(`route-${Routes.PREDICT.ROOT}`),
).toBeOnTheScreen();
+ expect(await findByTestId('predict-root-tab-trending')).toBeOnTheScreen();
+
+ // Assert — the `result_clicked` event fires with the tapped market's id/title
+ expect(
+ Engine.context.PredictController.trackSearchInteracted,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ interactionType: 'result_clicked',
+ searchQuery: 'bitcoin',
+ marketId: MOCK_PREDICT_MARKET.id,
+ marketTitle: MOCK_PREDICT_MARKET.title,
+ }),
+ );
searchMarketsSpy.mockRestore();
});
diff --git a/app/components/UI/Predict/views/PredictHome/PredictHome.tsx b/app/components/UI/Predict/views/PredictHome/PredictHome.tsx
index 08cdcc808a6..51b81ba6eb0 100644
--- a/app/components/UI/Predict/views/PredictHome/PredictHome.tsx
+++ b/app/components/UI/Predict/views/PredictHome/PredictHome.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useRef } from 'react';
import { LayoutChangeEvent } from 'react-native';
import Animated from 'react-native-reanimated';
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -7,13 +7,18 @@ import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { Box, Text, TextVariant } from '@metamask/design-system-react-native';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';
+import Engine from '../../../../../core/Engine';
import { useTheme } from '../../../../../util/theme';
+import { PredictEventValues } from '../../constants/eventNames';
import { usePredictSearch } from '../../hooks/usePredictSearch';
import { usePredictStackedHeader } from '../../hooks/usePredictStackedHeader';
import { PredictNavigationParamList } from '../../types/navigation';
import PredictHeaderStacked from '../../components/PredictHeaderStacked';
import PredictSearchOverlay from '../../components/PredictSearchOverlay';
-import PredictPortfolioModule from './components/PredictPortfolioModule';
+import PredictWithdrawUnavailableSheet, {
+ type PredictWithdrawUnavailableSheetRef,
+} from '../../components/PredictWithdrawUnavailableSheet';
+import { PredictPortfolioModule } from './components/PredictPortfolio';
import PredictLiveNowSection from './components/PredictLiveNowSection';
import PredictCategoriesSection from './components/PredictCategoriesSection';
import PredictPopularTodaySection from './components/PredictPopularTodaySection';
@@ -39,6 +44,11 @@ const PredictHome: React.FC = () => {
const route =
useRoute>();
const transactionActiveAbTests = route.params?.transactionActiveAbTests;
+ // Use the entry point the navigator passed; do NOT default. Falling back to a
+ // concrete value (e.g. `predict_feed`) would attribute home search engagement
+ // to the wrong surface. When unknown, the analytics mapper omits `entry_point`
+ // rather than bucketing it incorrectly.
+ const entryPoint = route.params?.entryPoint;
const { scrollY, titleSectionHeight, onScroll, setTitleSectionHeight } =
usePredictStackedHeader();
@@ -51,6 +61,14 @@ const PredictHome: React.FC = () => {
clearSearchAndClose,
} = usePredictSearch();
+ const handleShowSearch = useCallback(() => {
+ Engine.context.PredictController.trackSearchInteracted({
+ interactionType: PredictEventValues.SEARCH_INTERACTION.OPENED,
+ entryPoint,
+ });
+ showSearch();
+ }, [entryPoint, showSearch]);
+
const handleBackPress = useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack();
@@ -68,6 +86,12 @@ const PredictHome: React.FC = () => {
[setTitleSectionHeight],
);
+ const withdrawUnavailableSheetRef =
+ useRef(null);
+ const handleDepositWalletWithdrawPress = useCallback(() => {
+ withdrawUnavailableSheetRef.current?.onOpenBottomSheet();
+ }, []);
+
return (
{
scrollY={scrollY}
titleSectionHeight={titleSectionHeight}
onBack={handleBackPress}
- onSearchPress={showSearch}
+ onSearchPress={handleShowSearch}
/>
{
{
-
+
@@ -119,8 +145,12 @@ const PredictHome: React.FC = () => {
onSearchChange={setSearchQuery}
onClose={clearSearchAndClose}
transactionActiveAbTests={transactionActiveAbTests}
+ entryPoint={entryPoint}
/>
+
+
+
);
};
diff --git a/app/components/UI/Predict/views/PredictHome/PredictHome.view.test.tsx b/app/components/UI/Predict/views/PredictHome/PredictHome.view.test.tsx
index 061701663a6..5e4f7c6e30b 100644
--- a/app/components/UI/Predict/views/PredictHome/PredictHome.view.test.tsx
+++ b/app/components/UI/Predict/views/PredictHome/PredictHome.view.test.tsx
@@ -21,6 +21,7 @@ import {
PredictSearchSelectorsIDs,
} from '../../Predict.testIds';
import { PREDICT_HEADER_STACKED_TEST_IDS } from '../../components/PredictHeaderStacked';
+import { PREDICT_PORTFOLIO_TEST_IDS } from './components/PredictPortfolio';
import { MOCK_PREDICT_MARKET } from '../../../../../../tests/component-view/fixtures/predict';
const SEARCH_PLACEHOLDER = 'Search prediction markets';
@@ -86,11 +87,13 @@ describe('PredictHome', () => {
describe('shell composition', () => {
it('renders the large "Predictions" title', async () => {
- const { findByTestId } = renderPredictHomeView();
+ const { getByTestId } = renderPredictHomeView();
- expect(
- await findByTestId(PredictHomeSelectorsIDs.TITLE),
- ).toHaveTextContent(PREDICTIONS_TITLE);
+ await waitFor(() => {
+ expect(getByTestId(PredictHomeSelectorsIDs.TITLE)).toHaveTextContent(
+ PREDICTIONS_TITLE,
+ );
+ });
});
it('composes the section placeholders in Figma order', async () => {
@@ -125,6 +128,37 @@ describe('PredictHome', () => {
expect(await findByPlaceholderText(SEARCH_PLACEHOLDER)).toBeOnTheScreen();
});
+
+ it('tracks the search opened event when the user presses the search icon', async () => {
+ const { getByTestId } = renderPredictHomeView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ expect(
+ Engine.context.PredictController.trackSearchInteracted,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({ interactionType: 'opened' }),
+ );
+ });
+
+ it('does not attribute the opened event to predict_feed when no entry point was provided', async () => {
+ // The redesigned home is rendered without an `entryPoint` route param, so
+ // the event must not fall back to the legacy `predict_feed` surface.
+ const { getByTestId } = renderPredictHomeView();
+
+ fireEvent.press(getByTestId(PredictSearchSelectorsIDs.SEARCH_BUTTON));
+
+ const trackSearchInteracted = Engine.context.PredictController
+ .trackSearchInteracted as jest.Mock;
+ const openedCall = trackSearchInteracted.mock.calls
+ .map(
+ ([args]) => args as { interactionType?: string; entryPoint?: string },
+ )
+ .find((args) => args.interactionType === 'opened');
+
+ expect(openedCall).toBeDefined();
+ expect(openedCall?.entryPoint).toBeUndefined();
+ });
});
describe('stacked header scroll wiring', () => {
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolio.testIds.ts b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolio.testIds.ts
similarity index 100%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolio.testIds.ts
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolio.testIds.ts
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioAction.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioAction.tsx
similarity index 100%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioAction.tsx
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioAction.tsx
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioActions.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioActions.tsx
similarity index 95%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioActions.tsx
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioActions.tsx
index d960419eb23..b866126d160 100644
--- a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioActions.tsx
+++ b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioActions.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, IconName } from '@metamask/design-system-react-native';
-import { strings } from '../../../../../../locales/i18n';
+import { strings } from '../../../../../../../../locales/i18n';
import PredictPortfolioAction from './PredictPortfolioAction';
import { PREDICT_PORTFOLIO_TEST_IDS } from './PredictPortfolio.testIds';
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.test.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.test.tsx
similarity index 85%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.test.tsx
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.test.tsx
index 5c0c153a81b..a5af3281f29 100644
--- a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.test.tsx
+++ b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.test.tsx
@@ -1,10 +1,11 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
-import Routes from '../../../../../constants/navigation/Routes';
-import Engine from '../../../../../core/Engine';
-import renderWithProvider from '../../../../../util/test/renderWithProvider';
-import { PredictEventValues } from '../../constants/eventNames';
-import { usePredictPortfolio } from '../../hooks/usePredictPortfolio';
+import Routes from '../../../../../../../constants/navigation/Routes';
+import Engine from '../../../../../../../core/Engine';
+import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
+import { PredictHomeSelectorsIDs } from '../../../../Predict.testIds';
+import { PredictEventValues } from '../../../../constants/eventNames';
+import { usePredictPortfolio } from '../../../../hooks/usePredictPortfolio';
import PredictPortfolioModule from './PredictPortfolioModule';
import { PREDICT_PORTFOLIO_TEST_IDS } from './PredictPortfolio.testIds';
@@ -39,28 +40,28 @@ jest.mock('react-redux', () => ({
}),
}));
-jest.mock('../../../../../selectors/preferencesController', () => ({
+jest.mock('../../../../../../../selectors/preferencesController', () => ({
selectPrivacyMode: 'selectPrivacyMode',
}));
jest.mock(
- '../../../../../selectors/featureFlagController/confirmations',
+ '../../../../../../../selectors/featureFlagController/confirmations',
() => ({
selectMetaMaskPayFlags: 'selectMetaMaskPayFlags',
}),
);
-jest.mock('../../hooks/usePredictActionGuard', () => ({
+jest.mock('../../../../hooks/usePredictActionGuard', () => ({
usePredictActionGuard: () => ({
executeGuardedAction: mockExecuteGuardedAction,
}),
}));
-jest.mock('../../hooks/usePredictPortfolio', () => ({
+jest.mock('../../../../hooks/usePredictPortfolio', () => ({
usePredictPortfolio: jest.fn(),
}));
-jest.mock('../../../../../../locales/i18n', () => ({
+jest.mock('../../../../../../../../locales/i18n', () => ({
strings: jest.fn((key: string, params?: Record) => {
const mockStrings: Record = {
'predict.claim_amount_text': `Claim $${params?.amount}`,
@@ -150,7 +151,7 @@ describe('PredictPortfolioModule', () => {
renderWithProvider();
expect(
- screen.getByTestId(PREDICT_PORTFOLIO_TEST_IDS.MODULE),
+ screen.getByTestId(PredictHomeSelectorsIDs.PORTFOLIO_MODULE),
).toBeOnTheScreen();
expect(screen.getByText('$0.00')).toBeOnTheScreen();
expect(screen.getByText('Positions')).toBeOnTheScreen();
@@ -382,43 +383,13 @@ describe('PredictPortfolioModule', () => {
);
});
- it('does not track claim initiated when the action guard short-circuits', () => {
- mockUsePredictPortfolio.mockReturnValue(
- createPortfolio({
- claimableAmount: 46.35,
- hasClaimableWinnings: true,
- }),
- );
- mockExecuteGuardedAction.mockImplementationOnce(() => undefined);
-
- renderWithProvider();
-
- fireEvent.press(
- screen.getByTestId(PREDICT_PORTFOLIO_TEST_IDS.CLAIM_BUTTON),
- );
-
- expect(mockExecuteGuardedAction).toHaveBeenCalledWith(
- expect.any(Function),
- { attemptedAction: PredictEventValues.ATTEMPTED_ACTION.CLAIM },
- );
- expect(mockClaim).not.toHaveBeenCalled();
- expect(mockTrackPortfolioTransactionInitiated).not.toHaveBeenCalled();
- });
-
- it('uses the temporary Positions fallback until the route lands', () => {
+ it('navigates to the Predict Positions screen by default', () => {
renderWithProvider();
fireEvent.press(
screen.getByTestId(PREDICT_PORTFOLIO_TEST_IDS.ACTION_POSITIONS),
);
- expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.MARKET_LIST, {
- entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
- });
- expect(mockTrackPortfolioPositionsButtonTapped).toHaveBeenCalledWith(
- expectedPortfolioContext({
- entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
- }),
- );
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.POSITIONS);
});
});
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.tsx
similarity index 85%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.tsx
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.tsx
index 60580b5afdc..520be9a3ae7 100644
--- a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioModule.tsx
+++ b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioModule.tsx
@@ -2,18 +2,19 @@ import React, { useCallback, useMemo } from 'react';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { Box } from '@metamask/design-system-react-native';
import { useSelector } from 'react-redux';
-import Routes from '../../../../../constants/navigation/Routes';
-import Engine from '../../../../../core/Engine';
-import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
-import { selectPrivacyMode } from '../../../../../selectors/preferencesController';
-import { PredictEventValues } from '../../constants/eventNames';
-import { usePredictActionGuard } from '../../hooks/usePredictActionGuard';
-import { usePredictPortfolio } from '../../hooks/usePredictPortfolio';
-import type { PredictNavigationParamList } from '../../types/navigation';
-import PredictClaimButton from '../PredictActionButtons/PredictClaimButton';
+import Routes from '../../../../../../../constants/navigation/Routes';
+import Engine from '../../../../../../../core/Engine';
+import { selectMetaMaskPayFlags } from '../../../../../../../selectors/featureFlagController/confirmations';
+import { selectPrivacyMode } from '../../../../../../../selectors/preferencesController';
+import { PredictEventValues } from '../../../../constants/eventNames';
+import { usePredictActionGuard } from '../../../../hooks/usePredictActionGuard';
+import { usePredictPortfolio } from '../../../../hooks/usePredictPortfolio';
+import type { PredictNavigationParamList } from '../../../../types/navigation';
+import PredictClaimButton from '../../../../components/PredictActionButtons/PredictClaimButton';
import PredictPortfolioActions from './PredictPortfolioActions';
import PredictPortfolioSummary from './PredictPortfolioSummary';
import { PREDICT_PORTFOLIO_TEST_IDS } from './PredictPortfolio.testIds';
+import { PredictHomeSelectorsIDs } from '../../../../Predict.testIds';
export interface PredictPortfolioModuleProps {
onDepositWalletWithdrawPress?: () => void;
@@ -81,10 +82,7 @@ const PredictPortfolioModule: React.FC = ({
return;
}
- // Temporary fallback until the dedicated Predict Positions route lands.
- navigation.navigate(Routes.PREDICT.MARKET_LIST, {
- entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
- });
+ navigation.navigate(Routes.PREDICT.POSITIONS);
}, [navigation, onPositionsPress, portfolioAnalyticsProperties]);
const handleAddFundsPress = useCallback(() => {
@@ -156,7 +154,7 @@ const PredictPortfolioModule: React.FC = ({
const isWithdrawDisabled = availableBalance > 0 && !walletType;
return (
-
+
({
+jest.mock('../../../../../../../../locales/i18n', () => ({
strings: jest.fn((key: string, params?: Record) => {
const mockStrings: Record = {
'predict.portfolio.available_amount': `${params?.amount} available`,
diff --git a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioSummary.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioSummary.tsx
similarity index 92%
rename from app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioSummary.tsx
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioSummary.tsx
index b17479ddf74..35830c2434a 100644
--- a/app/components/UI/Predict/components/PredictPortfolio/PredictPortfolioSummary.tsx
+++ b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/PredictPortfolioSummary.tsx
@@ -8,19 +8,19 @@ import {
} from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { typography } from '@metamask/design-tokens';
-import { strings } from '../../../../../../locales/i18n';
+import { strings } from '../../../../../../../../locales/i18n';
import SensitiveText, {
SensitiveTextLength,
-} from '../../../../../component-library/components/Texts/SensitiveText';
+} from '../../../../../../../component-library/components/Texts/SensitiveText';
import {
TextColor as ComponentTextColor,
TextVariant as ComponentTextVariant,
-} from '../../../../../component-library/components/Texts/Text/Text.types';
-import { Skeleton } from '../../../../../component-library/components-temp/Skeleton';
+} from '../../../../../../../component-library/components/Texts/Text/Text.types';
+import { Skeleton } from '../../../../../../../component-library/components-temp/Skeleton';
import {
formatPredictUnrealizedPnLStringParts,
formatPrice,
-} from '../../utils/format';
+} from '../../../../utils/format';
import { PREDICT_PORTFOLIO_TEST_IDS } from './PredictPortfolio.testIds';
const PRIMARY_SKELETON_HEIGHT = typography.sDisplayLG.lineHeight;
diff --git a/app/components/UI/Predict/components/PredictPortfolio/index.ts b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/index.ts
similarity index 100%
rename from app/components/UI/Predict/components/PredictPortfolio/index.ts
rename to app/components/UI/Predict/views/PredictHome/components/PredictPortfolio/index.ts
diff --git a/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/PredictPortfolioModule.tsx b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/PredictPortfolioModule.tsx
deleted file mode 100644
index 7efb4fe8475..00000000000
--- a/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/PredictPortfolioModule.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-import {
- Box,
- Text,
- TextColor,
- TextVariant,
-} from '@metamask/design-system-react-native';
-import { strings } from '../../../../../../../../locales/i18n';
-import { PredictHomeSelectorsIDs } from '../../../../Predict.testIds';
-
-interface PredictPortfolioModuleProps {
- testID?: string;
-}
-
-/**
- * Placeholder for the Predict home portfolio module (PRED-835).
- * Replaced by the real module in a later ticket; the shell composes it as-is.
- */
-const PredictPortfolioModule: React.FC = ({
- testID = PredictHomeSelectorsIDs.PORTFOLIO_MODULE,
-}) => (
-
-
- {strings('predict.home.portfolio_module_placeholder')}
-
-
-);
-
-export default PredictPortfolioModule;
diff --git a/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/index.ts b/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/index.ts
deleted file mode 100644
index 7c5dbb921ea..00000000000
--- a/app/components/UI/Predict/views/PredictHome/components/PredictPortfolioModule/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './PredictPortfolioModule';
diff --git a/app/components/Views/AddressSelector/AddressSelector.test.tsx b/app/components/Views/AddressSelector/AddressSelector.test.tsx
index 7945af35f56..ea709a69843 100644
--- a/app/components/Views/AddressSelector/AddressSelector.test.tsx
+++ b/app/components/Views/AddressSelector/AddressSelector.test.tsx
@@ -142,7 +142,7 @@ describe('AccountSelector', () => {
ARBITRUM_DISPLAY_NAME,
BASE_DISPLAY_NAME,
BNB_DISPLAY_NAME,
- 'Monad Mainnet',
+ 'Monad',
OPTIMISM_DISPLAY_NAME,
POLYGON_DISPLAY_NAME,
]);
diff --git a/app/components/Views/Root/index.tsx b/app/components/Views/Root/index.tsx
index c9e2276fc13..6397fb43c70 100644
--- a/app/components/Views/Root/index.tsx
+++ b/app/components/Views/Root/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/lib/integration/react';
import { store, persistor } from '../../../store';
@@ -24,6 +24,11 @@ import { ReducedMotionConfig, ReduceMotion } from 'react-native-reanimated';
import { QueryClientProvider } from '@tanstack/react-query';
import reactQueryService from '../../../core/ReactQueryService';
import { HardwareWalletProvider } from '../../../core/HardwareWallet';
+import { UIMessengerProvider } from '../../../contexts/ui-messenger';
+import {
+ createUIMessenger,
+ UIMessenger,
+} from '../../../messengers/ui-messenger';
/**
* Top level of the component hierarchy
@@ -32,6 +37,14 @@ import { HardwareWalletProvider } from '../../../core/HardwareWallet';
const Root = ({ foxCode }: RootProps) => {
const [isStoreLoading, setIsStoreLoading] = useState(true);
+ // We use a ref to make sure the UI messenger is only created once.
+ const uiMessengerRef = useRef(null);
+ if (!uiMessengerRef.current) {
+ uiMessengerRef.current = createUIMessenger();
+ }
+
+ const uiMessenger = uiMessengerRef.current;
+
/**
* Wait for store to be initialized in Detox tests
* Note: This is a workaround for an issue with Detox where the store is not initialized
@@ -86,12 +99,14 @@ const Root = ({ foxCode }: RootProps) => {
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx
index fb6784fd744..47e3ace6884 100644
--- a/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx
+++ b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx
@@ -41,6 +41,8 @@ import { selectInternalAccounts } from '../../../../selectors/accountsController
import { InternalAccount } from '@metamask/keyring-internal-api';
import Logger from '../../../../util/Logger';
import { areAddressesEqual } from '../../../../util/address';
+import { RouteMessengerInstance } from './messenger';
+import { useMessenger } from '../../../../hooks/useMessenger';
interface SnapSettingsProps {
snap: Snap;
}
@@ -52,6 +54,7 @@ const SnapSettings = () => {
const { styles, theme } = useStyles(stylesheet, {});
const { colors } = theme;
const navigation = useNavigation();
+ const messenger = useMessenger();
const { snap } = useParams();
const permissionsState = useSelector(selectPermissionControllerState);
@@ -104,8 +107,7 @@ const SnapSettings = () => {
}, []);
const removeSnap = useCallback(async () => {
- const { SnapController } = Engine.context;
- await SnapController.removeSnap(snap.id);
+ await messenger.call('SnapController:removeSnap', snap.id);
if (isKeyringSnap && keyringAccounts.length > 0) {
try {
@@ -120,7 +122,7 @@ const SnapSettings = () => {
}
}
navigation.goBack();
- }, [isKeyringSnap, keyringAccounts, navigation, snap.id]);
+ }, [isKeyringSnap, keyringAccounts, navigation, messenger, snap.id]);
const handleRemoveSnap = useCallback(() => {
if (isKeyringSnap && keyringAccounts.length > 0) {
diff --git a/app/components/Views/Snaps/SnapSettings/index.ts b/app/components/Views/Snaps/SnapSettings/index.ts
index 3b7fda3a8a2..87f3a3e9d56 100644
--- a/app/components/Views/Snaps/SnapSettings/index.ts
+++ b/app/components/Views/Snaps/SnapSettings/index.ts
@@ -1,4 +1,5 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
/* eslint-disable import-x/prefer-default-export */
export { default as SnapSettings } from './SnapSettings';
+export { ALLOWED_CAPABILITIES } from './messenger';
///: END:ONLY_INCLUDE_IF
diff --git a/app/components/Views/Snaps/SnapSettings/messenger.ts b/app/components/Views/Snaps/SnapSettings/messenger.ts
new file mode 100644
index 00000000000..d55b38779c0
--- /dev/null
+++ b/app/components/Views/Snaps/SnapSettings/messenger.ts
@@ -0,0 +1,11 @@
+import { defineAllowedRouteCapabilities } from '../../../../messengers/helpers/route-messenger-helpers';
+import type { RouteMessengerFromCapabilities } from '../../../../messengers/route-messenger';
+
+export const ALLOWED_CAPABILITIES = defineAllowedRouteCapabilities({
+ actions: ['SnapController:removeSnap'],
+ events: [],
+});
+
+export type RouteMessengerInstance = RouteMessengerFromCapabilities<
+ typeof ALLOWED_CAPABILITIES
+>;
diff --git a/app/components/Views/Snaps/SnapSettings/test/SnapSettings.test.tsx b/app/components/Views/Snaps/SnapSettings/test/SnapSettings.test.tsx
index 36c5492ab0e..f0029dad4d8 100644
--- a/app/components/Views/Snaps/SnapSettings/test/SnapSettings.test.tsx
+++ b/app/components/Views/Snaps/SnapSettings/test/SnapSettings.test.tsx
@@ -30,6 +30,7 @@ import {
KEYRING_SNAP_REMOVAL_WARNING_CONTINUE,
KEYRING_SNAP_REMOVAL_WARNING_TEXT_INPUT,
} from '../../KeyringSnapRemovalWarning/KeyringSnapRemovalWarning.constants';
+import { createMockRouteMessenger } from '../../../../../util/test/mock-route-messenger';
const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
MOCK_ADDRESS_1,
@@ -219,9 +220,6 @@ jest.mock('../../../../../core/Engine', () => {
}),
removeAccount: jest.fn(),
context: {
- SnapController: {
- removeSnap: jest.fn(),
- },
KeyringController: {
state: {
keyrings: [
@@ -252,11 +250,16 @@ describe('SnapSettings with non keyring snap', () => {
});
it('renders correctly', () => {
+ const routeMessenger = createMockRouteMessenger({
+ 'SnapController:removeSnap': jest.fn(),
+ });
+
const { getAllByTestId, getByTestId } = renderWithProvider(
,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: initialState as any,
+ routeMessenger,
},
);
@@ -275,9 +278,14 @@ describe('SnapSettings with non keyring snap', () => {
});
it('calls navigation.goBack when header back button is pressed', () => {
+ const routeMessenger = createMockRouteMessenger({
+ 'SnapController:removeSnap': jest.fn(),
+ });
+
const { getByTestId } = renderWithProvider(, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: initialState as any,
+ routeMessenger,
});
fireEvent(getByTestId(SNAP_SETTINGS_BACK_BUTTON), 'onPress');
@@ -285,14 +293,22 @@ describe('SnapSettings with non keyring snap', () => {
});
it('remove snap and goes back when Remove button is pressed', async () => {
+ const routeMessenger = createMockRouteMessenger({
+ 'SnapController:removeSnap': jest.fn(),
+ });
+
+ jest.spyOn(routeMessenger, 'call');
+
const { getByTestId } = renderWithProvider(, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: initialState as any,
+ routeMessenger,
});
const removeButton = getByTestId(SNAP_SETTINGS_REMOVE_BUTTON);
fireEvent(removeButton, 'onPress');
- expect(Engine.context.SnapController.removeSnap).toHaveBeenCalledWith(
+ expect(routeMessenger.call).toHaveBeenCalledWith(
+ 'SnapController:removeSnap',
'npm:@chainsafe/filsnap',
);
await waitFor(() => {
@@ -407,9 +423,14 @@ describe('SnapSettings with keyring snap', () => {
};
it('renders KeyringSnapRemovalWarning when removing a keyring snap', async () => {
+ const routeMessenger = createMockRouteMessenger({
+ 'SnapController:removeSnap': jest.fn(),
+ });
+
const { getByTestId } = renderWithProvider(, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: initialStateWithKeyringSnap as any,
+ routeMessenger,
});
// Needed to allow for useEffect to run
@@ -423,10 +444,17 @@ describe('SnapSettings with keyring snap', () => {
});
});
- it('calls Engine.context.SnapController and Engine.removeAccount when removing a keyring snap with accounts', async () => {
+ it('calls `SnapController:removeSnap` when removing a keyring snap with accounts', async () => {
+ const routeMessenger = createMockRouteMessenger({
+ 'SnapController:removeSnap': jest.fn(),
+ });
+
+ jest.spyOn(routeMessenger, 'call');
+
const { getByTestId } = renderWithProvider(, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: initialStateWithKeyringSnap as any,
+ routeMessenger,
});
// Needed to allow for useEffect to run
@@ -456,7 +484,8 @@ describe('SnapSettings with keyring snap', () => {
// Step 5: Verify that the removal functions are called
await waitFor(() => {
- expect(Engine.context.SnapController.removeSnap).toHaveBeenCalledWith(
+ expect(routeMessenger.call).toHaveBeenCalledWith(
+ 'SnapController:removeSnap',
mockKeyringSnapId,
);
expect(Engine.removeAccount).toHaveBeenCalledTimes(2);
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyController.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyController.ts
index 7ab536f8780..3d5ba9fae1a 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyController.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyController.ts
@@ -36,8 +36,10 @@ import type {
QuickBuyTarget,
QuickBuyTradeMode,
} from '../types';
+import { getTokenKey } from '../tokenKey';
import { formatExchangeRate } from '../utils/formatExchangeRate';
import { getMetamaskFeePercent } from '../utils/getMetamaskFeePercent';
+import { selectDefaultReceiveToken } from '../utils/selectDefaultReceiveToken';
import { usePayWithTokens } from './usePayWithTokens';
import { usePositionTokenBalance } from './usePositionTokenBalance';
import { useQuickBuyAnalytics } from './useQuickBuyAnalytics';
@@ -93,7 +95,11 @@ import {
SocialLeaderboardEventValues,
} from '../../../../analytics';
import { buildQuickBuyToastOptions } from '../quickBuyToastOptions';
-import { trackQuickBuyTrade } from '../quickBuyTradeTracker';
+import {
+ trackQuickBuyTrade,
+ beginQuickBuySubmission,
+ endQuickBuySubmission,
+} from '../quickBuyTradeTracker';
import { resolveQuickBuyTerminalToast } from '../resolveQuickBuyTerminalToast';
export type QuickBuyButtonError =
@@ -231,6 +237,12 @@ export function useQuickBuyController(
const [tradeMode, setTradeMode] = useState('buy');
const [usdAmount, setUsdAmount] = useState('');
const [sourceAmountTokens, setSourceAmountTokens] = useState('');
+ // True when the user has committed the slider to 100% ("sell all"). In this
+ // mode `sourceTokenAmount` spends the exact on-chain balance rather than a
+ // value reconstructed from the fiat round-trip / float math, either of which
+ // can land just above the real balance and falsely trip the insufficient-
+ // funds gate. Reset on any other amount input.
+ const [isMaxSourceAmount, setIsMaxSourceAmount] = useState(false);
// Drives quote re-fetching. Updated only when the user commits a value
// (slider drag end, tap, or text input) — NOT on every drag tick. This
// prevents spamming quote requests while the thumb is moving.
@@ -348,20 +360,45 @@ export function useQuickBuyController(
const positionToken = usePositionTokenBalance(target, positionTokenFromSetup);
// ─── Sell "Receive" options (stablecoins) ──────────────────────────────
- const sellDestTokenOptions = useReceiveTokens(
+ const receiveTokenOptions = useReceiveTokens(
destChainId as string | undefined,
);
+ // Exclude the token being sold from the "Receive" list entirely — receiving
+ // the same token you're selling is a no-op, so it must not be selectable.
+ // Identity comes from `positionTokenFromSetup` (normalised address/chainId).
+ const sellDestTokenOptions = useMemo(() => {
+ if (!positionTokenFromSetup) return receiveTokenOptions;
+ const soldKey = getTokenKey(positionTokenFromSetup);
+ return receiveTokenOptions.filter(
+ (token) => getTokenKey(token) !== soldKey,
+ );
+ }, [receiveTokenOptions, positionTokenFromSetup]);
const [selectedDestStable, setSelectedDestStable] = useState<
BridgeToken | undefined
>(undefined);
- // Auto-select default dest stable: prefer a token the user already holds on
- // the position chain; fall back to the first candidate (USDC on position chain).
+ // Auto-select the default receive token. Prefer the native token of the
+ // position's chain (e.g. selling USDC on Base defaults to ETH on Base) and
+ // never the same token being sold — see `selectDefaultReceiveToken`.
+ //
+ // Wait for `!isSetupLoading` so the sold token's address is normalised before
+ // the (one-shot) selection runs. The sold token's identity is read from
+ // `positionTokenFromSetup` (which carries the normalised address/chainId)
+ // rather than the balance-enriched `positionToken`, so the exclusion still
+ // works even when the balance is still resolving.
useEffect(() => {
+ if (isSetupLoading) return;
if (sellDestTokenOptions.length > 0 && !selectedDestStable) {
- setSelectedDestStable(sellDestTokenOptions[0]);
+ setSelectedDestStable(
+ selectDefaultReceiveToken(sellDestTokenOptions, positionTokenFromSetup),
+ );
}
- }, [sellDestTokenOptions, selectedDestStable]);
+ }, [
+ isSetupLoading,
+ sellDestTokenOptions,
+ selectedDestStable,
+ positionTokenFromSetup,
+ ]);
// ─── Source / dest resolution (mode-dependent) ─────────────────────────
const sourceToken = tradeMode === 'buy' ? selectedSourceToken : positionToken;
@@ -403,7 +440,22 @@ export function useQuickBuyController(
sourceToken?.currencyExchangeRate && sourceToken.currencyExchangeRate > 0,
);
+ const latestSourceBalance = useLatestBalance({
+ address: sourceToken?.address,
+ decimals: sourceToken?.decimals,
+ chainId: sourceToken?.chainId,
+ balance: sourceToken?.balance,
+ });
+
const sourceTokenAmount = useMemo(() => {
+ // Max ("sell all"): spend the exact on-chain balance. `displayBalance` is
+ // `formatUnits(atomicBalance)`, so it round-trips back to the precise
+ // atomic balance — unlike the fiat (priced) or float (unpriced) paths
+ // below, which can reconstruct a value fractionally above the real balance
+ // and falsely block the trade with "Insufficient funds".
+ if (isMaxSourceAmount && latestSourceBalance?.displayBalance) {
+ return latestSourceBalance.displayBalance;
+ }
if (hasSourcePrice) {
if (!quotedUsdAmount || !sourceToken?.currencyExchangeRate) {
return undefined;
@@ -419,6 +471,8 @@ export function useQuickBuyController(
return sourceAmountTokens;
}, [
hasSourcePrice,
+ isMaxSourceAmount,
+ latestSourceBalance?.displayBalance,
quotedUsdAmount,
sourceAmountTokens,
sourceToken?.currencyExchangeRate,
@@ -432,13 +486,6 @@ export function useQuickBuyController(
}
}, [sourceTokenAmount, dispatch]);
- const latestSourceBalance = useLatestBalance({
- address: sourceToken?.address,
- decimals: sourceToken?.decimals,
- chainId: sourceToken?.chainId,
- balance: sourceToken?.balance,
- });
-
// Used for analytics passed to useQuickBuyQuotes. Must derive from
// quotedUsdAmount (not usdAmount) so that mid-drag display updates don't
// recreate quotesAnalyticsContext and trigger spurious quote re-fetches.
@@ -697,11 +744,15 @@ export function useQuickBuyController(
setSliderPercent(rounded);
// ── Unpriced path: drive token-amount state directly. ───────────────
+ // This branch commits on every tick (no separate drag-end commit), so the
+ // max sentinel is set here rather than in handleSliderDragEnd.
if (!hasSourcePrice) {
if (maxSpendTokens <= 0) {
setSourceAmountTokens('');
+ setIsMaxSourceAmount(false);
return;
}
+ setIsMaxSourceAmount(rounded >= 100);
const nextTokens =
rounded === 0 ? '' : ((maxSpendTokens * rounded) / 100).toString();
setSourceAmountTokens(nextTokens);
@@ -733,6 +784,15 @@ export function useQuickBuyController(
? ''
: ((maxSpendUsd * rounded) / 100).toFixed(2);
+ // Flag max BEFORE the dedup guard. lastCommittedUsdRef is also written by
+ // typed input and the price-migration effect, so releasing the slider at
+ // 100% can match the ref and return early (e.g. user typed the exact max,
+ // then slid to 100% to "sell all"). If setIsMaxSourceAmount ran after the
+ // guard, sourceTokenAmount would fall back to the cent-rounded fiat
+ // reconstruction and falsely trip insufficient-funds on sell-all. Setting
+ // it here is idempotent, so the dedup path is unaffected.
+ setIsMaxSourceAmount(rounded >= 100);
+
// Deduplicate: Tap + Pan can both fire onEnd for a pure tap gesture.
if (nextUsd === lastCommittedUsdRef.current) {
return;
@@ -792,6 +852,7 @@ export function useQuickBuyController(
setUsdAmount('');
setQuotedUsdAmount('');
setSourceAmountTokens('');
+ setIsMaxSourceAmount(false);
setSliderPercent(0);
lastSliderPercentRef.current = 0;
lastCommittedUsdRef.current = '';
@@ -882,6 +943,7 @@ export function useQuickBuyController(
}
lastSliderPercentRef.current = 0;
setSliderPercent(0);
+ setIsMaxSourceAmount(false);
},
[hasSourcePrice, sourceToken?.decimals, lastInputMethodRef],
);
@@ -1003,6 +1065,11 @@ export function useQuickBuyController(
submitStartedAtRef.current ? Date.now() - submitStartedAtRef.current : 0;
try {
+ // Mark a QuickBuy submission as in flight BEFORE submitTx so the generic
+ // transaction notification (which fires mid-submit, before the tx id is
+ // known) is suppressed. The id-based tracker takes over for the terminal
+ // notification once submitTx resolves.
+ beginQuickBuySubmission();
dispatch(setIsSubmittingTx(true));
const submitResult = await Engine.context.BridgeStatusController.submitTx(
walletAddress,
@@ -1074,6 +1141,10 @@ export function useQuickBuyController(
});
}
} finally {
+ // Cleared after `trackQuickBuyTrade` (in the try block) has registered the
+ // tx id, so the predicate transitions from marker-based to id-based
+ // suppression with no coverage gap.
+ endQuickBuySubmission();
dispatch(setIsSubmittingTx(false));
}
}, [
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.test.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.test.tsx
index 3e493a9dc2a..527433055ec 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.test.tsx
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.test.tsx
@@ -8,13 +8,21 @@ import {
} from '../../../../../../../util/haptics';
import { buildQuickBuyToastOptions } from '../quickBuyToastOptions';
import {
+ clearSettledQuickBuyTrades,
getTrackedQuickBuyTradeIds,
+ isQuickBuyTransaction,
trackQuickBuyTrade,
untrackQuickBuyTrade,
type TrackedQuickBuyTrade,
} from '../quickBuyTradeTracker';
+import { registerNotificationSkipPredicate } from '../../../../../../../core/notificationSkipPredicates';
import { useQuickBuyToastRegistrations } from './useQuickBuyToastRegistrations';
+jest.mock('../../../../../../../core/notificationSkipPredicates', () => ({
+ __esModule: true,
+ registerNotificationSkipPredicate: jest.fn(() => jest.fn()),
+}));
+
jest.mock('../quickBuyToastOptions', () => ({
buildQuickBuyToastOptions: jest.fn((kind: string) => ({ kind })),
}));
@@ -101,10 +109,29 @@ describe('useQuickBuyToastRegistrations', () => {
beforeEach(() => {
jest.clearAllMocks();
getTrackedQuickBuyTradeIds().forEach(untrackQuickBuyTrade);
+ clearSettledQuickBuyTrades();
Engine.context.MultichainTransactionsController.state.nonEvmTransactions =
{};
});
+ it('registers the QuickBuy notification skip predicate on mount and unregisters on unmount', () => {
+ const unregister = jest.fn();
+ (registerNotificationSkipPredicate as jest.Mock).mockReturnValue(
+ unregister,
+ );
+
+ const { unmount } = renderHook(() => useQuickBuyToastRegistrations());
+
+ expect(registerNotificationSkipPredicate).toHaveBeenCalledWith(
+ isQuickBuyTransaction,
+ );
+ expect(unregister).not.toHaveBeenCalled();
+
+ unmount();
+
+ expect(unregister).toHaveBeenCalledTimes(1);
+ });
+
it('subscribes to both the bridge and multichain state change events', () => {
const { result } = renderHook(() => useQuickBuyToastRegistrations());
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.tsx
index 49d92e2e431..cf2bcc97098 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.tsx
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/hooks/useQuickBuyToastRegistrations.tsx
@@ -1,8 +1,12 @@
-import { useCallback, useMemo } from 'react';
+import { useCallback, useEffect, useMemo } from 'react';
import type { ToastRef } from '../../../../../../../component-library/components/Toast/Toast.types';
import { useAppThemeFromContext } from '../../../../../../../util/theme';
+import { registerNotificationSkipPredicate } from '../../../../../../../core/notificationSkipPredicates';
import type { ToastRegistration } from '../../../../../../Nav/App/ControllerEventToastBridge';
-import { getTrackedQuickBuyTradeIds } from '../quickBuyTradeTracker';
+import {
+ getTrackedQuickBuyTradeIds,
+ isQuickBuyTransaction,
+} from '../quickBuyTradeTracker';
import { resolveQuickBuyTerminalToast } from '../resolveQuickBuyTerminalToast';
/**
@@ -19,6 +23,11 @@ import { resolveQuickBuyTerminalToast } from '../resolveQuickBuyTerminalToast';
export const useQuickBuyToastRegistrations = (): ToastRegistration[] => {
const theme = useAppThemeFromContext();
+ // Opt QuickBuy-initiated transactions out of the generic transaction
+ // notifications so the user only sees QuickBuy's own toasts. Registered at
+ // the app root so it covers submissions regardless of sheet lifecycle.
+ useEffect(() => registerNotificationSkipPredicate(isQuickBuyTransaction), []);
+
// Shared by both controller subscriptions: `resolveQuickBuyTerminalToast`
// checks each tracked trade against whichever controller is authoritative for
// it, so we can fan out the same scan regardless of which event fired.
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.test.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.test.ts
index 2b336628600..d90c0d69f3b 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.test.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.test.ts
@@ -1,11 +1,24 @@
import {
+ TransactionType,
+ type TransactionMeta,
+} from '@metamask/transaction-controller';
+import {
+ beginQuickBuySubmission,
+ clearSettledQuickBuyTrades,
+ endQuickBuySubmission,
getTrackedQuickBuyTrade,
getTrackedQuickBuyTradeIds,
+ hasPendingQuickBuySubmission,
+ isQuickBuyTransaction,
+ markQuickBuyTradeSettled,
trackQuickBuyTrade,
untrackQuickBuyTrade,
type TrackedQuickBuyTrade,
} from './quickBuyTradeTracker';
+const txMeta = (overrides: Partial): TransactionMeta =>
+ overrides as TransactionMeta;
+
const buyTrade: TrackedQuickBuyTrade = {
tradeMode: 'buy',
tokenSymbol: 'PEPE',
@@ -24,6 +37,10 @@ const sellTrade: TrackedQuickBuyTrade = {
describe('quickBuyTradeTracker', () => {
afterEach(() => {
getTrackedQuickBuyTradeIds().forEach(untrackQuickBuyTrade);
+ clearSettledQuickBuyTrades();
+ while (hasPendingQuickBuySubmission()) {
+ endQuickBuySubmission();
+ }
});
it('stores and returns a tracked trade by tx meta id', () => {
@@ -84,4 +101,146 @@ describe('quickBuyTradeTracker', () => {
expect(() => untrackQuickBuyTrade('missing')).not.toThrow();
expect(getTrackedQuickBuyTradeIds()).toEqual(['tx-1']);
});
+
+ describe('submission marker', () => {
+ it('reflects in-flight submissions via the counter', () => {
+ expect(hasPendingQuickBuySubmission()).toBe(false);
+
+ beginQuickBuySubmission();
+ expect(hasPendingQuickBuySubmission()).toBe(true);
+
+ endQuickBuySubmission();
+ expect(hasPendingQuickBuySubmission()).toBe(false);
+ });
+
+ it('stays active until all overlapping submissions end', () => {
+ beginQuickBuySubmission();
+ beginQuickBuySubmission();
+
+ endQuickBuySubmission();
+ expect(hasPendingQuickBuySubmission()).toBe(true);
+
+ endQuickBuySubmission();
+ expect(hasPendingQuickBuySubmission()).toBe(false);
+ });
+
+ it('never drops below zero on unbalanced ends', () => {
+ endQuickBuySubmission();
+
+ expect(hasPendingQuickBuySubmission()).toBe(false);
+
+ beginQuickBuySubmission();
+ expect(hasPendingQuickBuySubmission()).toBe(true);
+ });
+ });
+
+ describe('isQuickBuyTransaction', () => {
+ it('matches a transaction already tracked by id', () => {
+ trackQuickBuyTrade('tx-1', buyTrade);
+
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-1' }))).toBe(true);
+ });
+
+ it('matches an in-flight swap/bridge/batch transaction during submission', () => {
+ beginQuickBuySubmission();
+
+ expect(
+ isQuickBuyTransaction(
+ txMeta({ id: 'tx-9', type: TransactionType.batch }),
+ ),
+ ).toBe(true);
+ expect(
+ isQuickBuyTransaction(
+ txMeta({ id: 'tx-9', type: TransactionType.swap }),
+ ),
+ ).toBe(true);
+ expect(
+ isQuickBuyTransaction(
+ txMeta({ id: 'tx-9', type: TransactionType.bridge }),
+ ),
+ ).toBe(true);
+ });
+
+ it('matches a batch whose nested transaction is a swap during submission', () => {
+ beginQuickBuySubmission();
+
+ expect(
+ isQuickBuyTransaction(
+ txMeta({
+ id: 'tx-9',
+ type: TransactionType.batch,
+ nestedTransactions: [{ type: TransactionType.swap }],
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any),
+ ),
+ ).toBe(true);
+ });
+
+ it('does not match an unrelated in-flight transaction type', () => {
+ beginQuickBuySubmission();
+
+ expect(
+ isQuickBuyTransaction(
+ txMeta({ id: 'tx-9', type: TransactionType.simpleSend }),
+ ),
+ ).toBe(false);
+ });
+
+ it('does not match when there is no marker and no tracked id', () => {
+ expect(
+ isQuickBuyTransaction(
+ txMeta({ id: 'tx-9', type: TransactionType.swap }),
+ ),
+ ).toBe(false);
+ });
+
+ it('still matches a trade after it has settled (covers the delayed re-check)', () => {
+ trackQuickBuyTrade('tx-1', buyTrade);
+ markQuickBuyTradeSettled('tx-1');
+
+ expect(getTrackedQuickBuyTradeIds()).toEqual([]);
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-1' }))).toBe(true);
+ });
+
+ it('stops matching a settled trade once it is explicitly untracked', () => {
+ trackQuickBuyTrade('tx-1', buyTrade);
+ markQuickBuyTradeSettled('tx-1');
+ untrackQuickBuyTrade('tx-1');
+
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-1' }))).toBe(false);
+ });
+ });
+
+ describe('markQuickBuyTradeSettled', () => {
+ it('removes the trade from the active registry so the terminal toast does not repeat', () => {
+ trackQuickBuyTrade('tx-1', buyTrade);
+
+ markQuickBuyTradeSettled('tx-1');
+
+ expect(getTrackedQuickBuyTrade('tx-1')).toBeUndefined();
+ expect(getTrackedQuickBuyTradeIds()).toEqual([]);
+ });
+
+ it('evicts the oldest settled id once the retention cap is exceeded', () => {
+ for (let i = 0; i < 51; i += 1) {
+ markQuickBuyTradeSettled(`tx-${i}`);
+ }
+
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-0' }))).toBe(false);
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-1' }))).toBe(true);
+ expect(isQuickBuyTransaction(txMeta({ id: 'tx-50' }))).toBe(true);
+ });
+
+ it('refreshes a re-settled id so it survives further eviction', () => {
+ markQuickBuyTradeSettled('keep-me');
+ for (let i = 0; i < 49; i += 1) {
+ markQuickBuyTradeSettled(`tx-${i}`);
+ }
+ // Re-settle to move it to the newest position before overflowing.
+ markQuickBuyTradeSettled('keep-me');
+ markQuickBuyTradeSettled('overflow');
+
+ expect(isQuickBuyTransaction(txMeta({ id: 'keep-me' }))).toBe(true);
+ });
+ });
});
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.ts
index 85cbd6f9c17..5806ad85934 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuyTradeTracker.ts
@@ -1,3 +1,7 @@
+import {
+ TransactionType,
+ type TransactionMeta,
+} from '@metamask/transaction-controller';
import type { QuickBuyTradeMode } from './types';
export interface TrackedQuickBuyTrade {
@@ -41,6 +45,30 @@ const trackedTrades = new Map();
/** Shared empty result so the common "nothing tracked" path allocates nothing. */
const EMPTY_TRADE_IDS: string[] = [];
+/**
+ * Ids of QuickBuy trades that have reached a terminal state and been untracked.
+ *
+ * The terminal handler untracks a trade the instant the swap settles, but
+ * `NotificationManager` re-checks the skip predicates ~2s *after*
+ * `transactionConfirmed`/`transactionFailed` (a delayed `setTimeout`). For a
+ * fast-settling QuickBuy the trade would already be gone from `trackedTrades`
+ * by then, so the generic success/error toast would leak on top of QuickBuy's
+ * own terminal toast. Remembering recently-settled ids keeps suppression alive
+ * across that window.
+ *
+ * Bounded (insertion-ordered FIFO eviction) so it can never grow unbounded;
+ * QuickBuy ids are globally-unique tx meta ids, so retaining a small recent
+ * history is harmless.
+ */
+const settledTradeIds = new Set();
+
+/**
+ * Upper bound on remembered settled ids. Comfortably larger than any realistic
+ * number of QuickBuy trades settling within the ~2s notification re-check
+ * window, while keeping memory trivially small.
+ */
+const SETTLED_TRADE_ID_LIMIT = 50;
+
export function trackQuickBuyTrade(
txMetaId: string,
info: TrackedQuickBuyTrade,
@@ -66,4 +94,100 @@ export function getTrackedQuickBuyTradeIds(): string[] {
export function untrackQuickBuyTrade(txMetaId: string): void {
trackedTrades.delete(txMetaId);
+ settledTradeIds.delete(txMetaId);
+}
+
+/**
+ * Marks a trade as terminally settled: removes it from the active registry (so
+ * the terminal toast is not emitted twice) while remembering its id so the
+ * delayed generic-notification re-check still suppresses the leftover
+ * success/error toast.
+ */
+export function markQuickBuyTradeSettled(txMetaId: string): void {
+ trackedTrades.delete(txMetaId);
+
+ // Re-insert to refresh FIFO position, then evict the oldest if over the cap.
+ settledTradeIds.delete(txMetaId);
+ settledTradeIds.add(txMetaId);
+ if (settledTradeIds.size > SETTLED_TRADE_ID_LIMIT) {
+ const oldest = settledTradeIds.values().next().value;
+ if (oldest !== undefined) {
+ settledTradeIds.delete(oldest);
+ }
+ }
+}
+
+/** Clears all remembered settled ids. Intended for test isolation. */
+export function clearSettledQuickBuyTrades(): void {
+ settledTradeIds.clear();
+}
+
+/**
+ * Count of QuickBuy submissions currently in flight.
+ *
+ * The source transaction meta id is only known once `submitTx` resolves, but
+ * the generic transaction notification fires earlier (on `transactionApproved`,
+ * mid-submit). This marker lets us suppress that early notification before the
+ * id is registered. A counter (rather than a boolean) tolerates overlapping
+ * submissions without prematurely clearing the marker.
+ */
+let pendingSubmissions = 0;
+
+export function beginQuickBuySubmission(): void {
+ pendingSubmissions += 1;
+}
+
+export function endQuickBuySubmission(): void {
+ pendingSubmissions = Math.max(0, pendingSubmissions - 1);
+}
+
+export function hasPendingQuickBuySubmission(): boolean {
+ return pendingSubmissions > 0;
+}
+
+// Transaction types a QuickBuy submission can produce. Used to narrow the
+// marker window so an unrelated transaction approving mid-submit is not
+// suppressed. `batch` is the 7702 wrapper that triggers the duplicate
+// notification; swap/bridge cover the non-batched paths.
+const QUICK_BUY_SUPPRESSED_TYPES: TransactionType[] = [
+ TransactionType.batch,
+ TransactionType.swap,
+ TransactionType.bridge,
+];
+
+/**
+ * Predicate consumed by `NotificationManager` to skip the generic transaction
+ * notification for QuickBuy-initiated trades. Matches a transaction that is
+ * either currently tracked by id, recently settled by id (covers the delayed
+ * generic-toast re-check that fires after the trade is untracked), or — while a
+ * submission is in flight — a swap/bridge/batch transaction (covers the pending
+ * notification that fires before the id is known).
+ */
+export function isQuickBuyTransaction(
+ transactionMeta: TransactionMeta | undefined,
+): boolean {
+ if (transactionMeta?.id) {
+ if (getTrackedQuickBuyTrade(transactionMeta.id)) {
+ return true;
+ }
+ if (settledTradeIds.has(transactionMeta.id)) {
+ return true;
+ }
+ }
+
+ if (!hasPendingQuickBuySubmission()) {
+ return false;
+ }
+
+ const { type, nestedTransactions } = transactionMeta ?? {};
+
+ if (type && QUICK_BUY_SUPPRESSED_TYPES.includes(type)) {
+ return true;
+ }
+
+ return Boolean(
+ nestedTransactions?.some(
+ (tx) => tx.type && QUICK_BUY_SUPPRESSED_TYPES.includes(tx.type),
+ ),
+ );
}
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.test.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.test.ts
index afcdc42a394..c1cbb1927e2 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.test.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.test.ts
@@ -7,7 +7,9 @@ import {
} from '../../../../../../util/haptics';
import { buildQuickBuyToastOptions } from './quickBuyToastOptions';
import {
+ clearSettledQuickBuyTrades,
getTrackedQuickBuyTradeIds,
+ isQuickBuyTransaction,
trackQuickBuyTrade,
untrackQuickBuyTrade,
type TrackedQuickBuyTrade,
@@ -83,6 +85,7 @@ describe('resolveQuickBuyTerminalToast', () => {
beforeEach(() => {
jest.clearAllMocks();
getTrackedQuickBuyTradeIds().forEach(untrackQuickBuyTrade);
+ clearSettledQuickBuyTrades();
Engine.context.MultichainTransactionsController.state.nonEvmTransactions =
{};
});
@@ -160,6 +163,23 @@ describe('resolveQuickBuyTerminalToast', () => {
expect(showToast).toHaveBeenCalledTimes(1);
});
+ it('keeps the trade recognised as QuickBuy after settling so the delayed generic toast stays suppressed', () => {
+ trackQuickBuyTrade('tx-1', buyTrade);
+ mockGetHistoryItem.mockReturnValue(
+ historyItemWithStatus(StatusTypes.COMPLETE),
+ );
+
+ resolveQuickBuyTerminalToast('tx-1', jest.fn(), theme);
+
+ expect(getTrackedQuickBuyTradeIds()).toEqual([]);
+ expect(
+ isQuickBuyTransaction({
+ id: 'tx-1',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any),
+ ).toBe(true);
+ });
+
it('resolves a Solana swap to complete from the multichain controller when bridge status is absent', () => {
trackQuickBuyTrade('sig-1', solanaTrade);
mockGetHistoryItem.mockReturnValue(undefined);
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.ts
index 8b5de26aea2..e2f66b0b045 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/resolveQuickBuyTerminalToast.ts
@@ -10,7 +10,7 @@ import type { Theme } from '../../../../../../util/theme/models';
import { buildQuickBuyToastOptions } from './quickBuyToastOptions';
import {
getTrackedQuickBuyTrade,
- untrackQuickBuyTrade,
+ markQuickBuyTradeSettled,
type TrackedQuickBuyTrade,
} from './quickBuyTradeTracker';
@@ -65,11 +65,13 @@ function resolveFromMultichain(signature: string): TerminalOutcome | undefined {
}
/**
- * Claims the trade atomically (untrack doubles as the dedupe), surfaces the
+ * Claims the trade atomically (settling doubles as the dedupe), surfaces the
* matching `complete` / `failed` toast and fires the paired haptic. The first
- * caller to reach a terminal status removes the trade, so any later
+ * caller to reach a terminal status settles the trade, so any later
* `stateChange` emission — or a concurrent resolve from the other controller —
- * becomes a no-op.
+ * becomes a no-op. Settling (rather than a plain untrack) keeps the id known to
+ * `isQuickBuyTransaction` so the delayed generic success/error toast is still
+ * suppressed.
*/
function emitTerminalToast(
txMetaId: string,
@@ -78,7 +80,7 @@ function emitTerminalToast(
showToast: ToastRef['showToast'],
theme: Theme,
): boolean {
- untrackQuickBuyTrade(txMetaId);
+ markQuickBuyTradeSettled(txMetaId);
const isComplete = outcome === 'complete';
showToast(
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/tokenKey.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/tokenKey.ts
index de6e7bd55ba..ff94f020554 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/tokenKey.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/tokenKey.ts
@@ -6,5 +6,6 @@ import type { BridgeToken } from '../../../../../UI/Bridge/types';
* both the "Pay with" (buy) and "Receive" (sell) flows so row keys and
* selection comparisons behave identically.
*/
-export const getTokenKey = (token: BridgeToken): string =>
- `${token.address.toLowerCase()}:${token.chainId}`;
+export const getTokenKey = (
+ token: Pick,
+): string => `${token.address.toLowerCase()}:${token.chainId}`;
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/useQuickBuyController.test.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/useQuickBuyController.test.ts
index 13821e0574d..2b2d50b5035 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/useQuickBuyController.test.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/useQuickBuyController.test.ts
@@ -40,7 +40,11 @@ import {
import { useQuickBuySetup } from './hooks/useQuickBuySetup';
import { useReceiveTokens } from './hooks/useReceiveTokens';
import { buildQuickBuyToastOptions } from './quickBuyToastOptions';
-import { trackQuickBuyTrade } from './quickBuyTradeTracker';
+import {
+ trackQuickBuyTrade,
+ beginQuickBuySubmission,
+ endQuickBuySubmission,
+} from './quickBuyTradeTracker';
import { resolveQuickBuyTerminalToast } from './resolveQuickBuyTerminalToast';
import { positionToQuickBuyTarget } from './types';
@@ -233,6 +237,8 @@ jest.mock('./quickBuyTradeTracker', () => ({
getTrackedQuickBuyTrade: jest.fn(),
getTrackedQuickBuyTradeIds: jest.fn(() => []),
untrackQuickBuyTrade: jest.fn(),
+ beginQuickBuySubmission: jest.fn(),
+ endQuickBuySubmission: jest.fn(),
}));
jest.mock('./quickBuyToastOptions', () => ({
@@ -587,6 +593,93 @@ describe('useQuickBuyController', () => {
});
});
+ describe('max ("sell all") source amount', () => {
+ // A near-$1 stablecoin whose rate is fractionally below 1. The fiat
+ // round-trip (balance → cent-rounded USD → tokens) reconstructs an amount
+ // slightly ABOVE the real balance, which previously tripped the
+ // insufficient-funds gate when selling the entire balance.
+ const NEAR_DOLLAR_RATE = 0.9997;
+ const FULL_BALANCE = '0.10003';
+
+ const renderWithStablecoin = () => {
+ (useLatestBalance as jest.Mock).mockReturnValue({
+ displayBalance: FULL_BALANCE,
+ atomicBalance: '100030000000000000',
+ });
+ const stablecoin = createSourceToken({
+ symbol: 'MUSD',
+ currencyExchangeRate: NEAR_DOLLAR_RATE,
+ balance: FULL_BALANCE,
+ });
+ (usePayWithTokens as jest.Mock).mockReturnValue({
+ options: [stablecoin],
+ });
+ return renderHook(() => useQuickBuyController(createTarget(), jest.fn()));
+ };
+
+ it('spends the exact on-chain balance instead of the fiat round-trip at 100%', () => {
+ const { result } = renderWithStablecoin();
+
+ act(() => {
+ result.current.handleSliderChange(100);
+ });
+ act(() => {
+ result.current.handleSliderDragEnd(100);
+ });
+
+ // Exact balance — not `usd / rate`, which would exceed FULL_BALANCE.
+ expect(result.current.sourceTokenAmount).toBe(FULL_BALANCE);
+ });
+
+ it('passes the exact balance to the insufficient-balance check at 100%', () => {
+ const { result } = renderWithStablecoin();
+
+ act(() => {
+ result.current.handleSliderChange(100);
+ });
+ act(() => {
+ result.current.handleSliderDragEnd(100);
+ });
+
+ expect(useIsInsufficientBalance).toHaveBeenLastCalledWith(
+ expect.objectContaining({ amount: FULL_BALANCE }),
+ );
+ });
+
+ it('still derives the amount from fiat below 100%', () => {
+ const { result } = renderWithStablecoin();
+
+ act(() => {
+ result.current.handleSliderChange(50);
+ });
+ act(() => {
+ result.current.handleSliderDragEnd(50);
+ });
+
+ expect(result.current.sourceTokenAmount).not.toBe(FULL_BALANCE);
+ expect(Number(result.current.sourceTokenAmount)).toBeCloseTo(0.05, 2);
+ });
+
+ it('clears the max flag once the user types a custom amount', () => {
+ const { result } = renderWithStablecoin();
+
+ act(() => {
+ result.current.handleSliderChange(100);
+ });
+ act(() => {
+ result.current.handleSliderDragEnd(100);
+ });
+ expect(result.current.sourceTokenAmount).toBe(FULL_BALANCE);
+
+ act(() => {
+ result.current.handleAmountChange('0.05');
+ });
+
+ expect(result.current.sourceTokenAmount).not.toBe(FULL_BALANCE);
+ expect(Number(result.current.sourceTokenAmount)).toBeCloseTo(0.05, 2);
+ });
+ });
+
describe('handleSelectSourceToken', () => {
it('updates the selected token and resets amount + slider state', () => {
(useLatestBalance as jest.Mock).mockReturnValue({
@@ -1424,6 +1517,109 @@ describe('useQuickBuyController', () => {
});
});
+ describe('receive token auto-selection', () => {
+ const NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000';
+ const USDC_DEST = '0xDEST'; // matches the default-mock position token
+
+ it('defaults the receive token to the chain native instead of the token being sold', () => {
+ // Selling 0xDEST on chain 0x1. The receive options are stablecoins-first,
+ // so options[0] is the very token being sold — the default must skip it
+ // and pick the chain's native token instead.
+ const usdcDest = createSourceToken({
+ address: USDC_DEST,
+ chainId: '0x1',
+ symbol: 'USDC',
+ });
+ const nativeDest = createSourceToken({
+ address: NATIVE_ADDRESS,
+ chainId: '0x1',
+ symbol: 'ETH',
+ });
+ (useReceiveTokens as jest.Mock).mockReturnValue([usdcDest, nativeDest]);
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ expect(result.current.selectedDestStable?.symbol).toBe('ETH');
+ });
+
+ it('falls back to the first non-sold candidate when selling the native token', () => {
+ (useQuickBuySetup as jest.Mock).mockReturnValue({
+ chainId: '0x1',
+ destToken: {
+ address: NATIVE_ADDRESS,
+ chainId: '0x1',
+ decimals: 18,
+ symbol: 'ETH',
+ name: 'Ether',
+ },
+ isLoading: false,
+ isUnsupportedChain: false,
+ });
+ const nativeDest = createSourceToken({
+ address: NATIVE_ADDRESS,
+ chainId: '0x1',
+ symbol: 'ETH',
+ });
+ const usdcDest = createSourceToken({
+ address: USDC_DEST,
+ chainId: '0x1',
+ symbol: 'USDC',
+ });
+ (useReceiveTokens as jest.Mock).mockReturnValue([nativeDest, usdcDest]);
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ expect(result.current.selectedDestStable?.symbol).toBe('USDC');
+ });
+
+ it('excludes the token being sold from the receive options entirely', () => {
+ const usdcDest = createSourceToken({
+ address: USDC_DEST,
+ chainId: '0x1',
+ symbol: 'USDC',
+ });
+ const nativeDest = createSourceToken({
+ address: NATIVE_ADDRESS,
+ chainId: '0x1',
+ symbol: 'ETH',
+ });
+ (useReceiveTokens as jest.Mock).mockReturnValue([usdcDest, nativeDest]);
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ const symbols = result.current.sellDestTokenOptions.map((t) => t.symbol);
+ expect(symbols).not.toContain('USDC');
+ expect(symbols).toEqual(['ETH']);
+ });
+
+ it('does not auto-select while setup is still loading', () => {
+ (useQuickBuySetup as jest.Mock).mockReturnValue({
+ chainId: '0x1',
+ destToken: undefined,
+ isLoading: true,
+ isUnsupportedChain: false,
+ });
+ const usdcDest = createSourceToken({
+ address: USDC_DEST,
+ chainId: '0x1',
+ symbol: 'USDC',
+ });
+ (useReceiveTokens as jest.Mock).mockReturnValue([usdcDest]);
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ expect(result.current.selectedDestStable).toBeUndefined();
+ });
+ });
+
describe('selectDefaultSourceToken', () => {
const native = (chainId: string, fiat = 1000): BridgeToken =>
createSourceToken({
@@ -2228,6 +2424,50 @@ describe('useQuickBuyController', () => {
expect(playErrorNotification).toHaveBeenCalledTimes(1);
expect(trackQuickBuyTrade).not.toHaveBeenCalled();
});
+
+ it('marks the submission in flight before submit and clears it after success', async () => {
+ mockUsableQuote();
+ (
+ Engine.context.BridgeStatusController.submitTx as jest.Mock
+ ).mockResolvedValue({ id: 'tx-1', hash: '0xabc' });
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ await act(async () => {
+ await result.current.handleConfirm();
+ });
+
+ expect(beginQuickBuySubmission).toHaveBeenCalledTimes(1);
+ expect(endQuickBuySubmission).toHaveBeenCalledTimes(1);
+ const beginOrder = (beginQuickBuySubmission as jest.Mock).mock
+ .invocationCallOrder[0];
+ const trackOrder = (trackQuickBuyTrade as jest.Mock).mock
+ .invocationCallOrder[0];
+ const endOrder = (endQuickBuySubmission as jest.Mock).mock
+ .invocationCallOrder[0];
+ expect(beginOrder).toBeLessThan(trackOrder);
+ expect(trackOrder).toBeLessThan(endOrder);
+ });
+
+ it('clears the in-flight submission marker when submit throws', async () => {
+ mockUsableQuote();
+ (
+ Engine.context.BridgeStatusController.submitTx as jest.Mock
+ ).mockRejectedValue(new Error('user rejected'));
+
+ const { result } = renderHook(() =>
+ useQuickBuyController(createTarget(), jest.fn()),
+ );
+
+ await act(async () => {
+ await result.current.handleConfirm();
+ });
+
+ expect(beginQuickBuySubmission).toHaveBeenCalledTimes(1);
+ expect(endQuickBuySubmission).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.test.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.test.ts
new file mode 100644
index 00000000000..6669a68d016
--- /dev/null
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.test.ts
@@ -0,0 +1,73 @@
+import type { Hex } from '@metamask/utils';
+import type { BridgeToken } from '../../../../../../UI/Bridge/types';
+import { selectDefaultReceiveToken } from './selectDefaultReceiveToken';
+
+const BASE = '0x2105' as Hex;
+const POLYGON = '0x89' as Hex;
+const NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000';
+const USDC_BASE = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
+const USDC_POLYGON = '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';
+const USDT_POLYGON = '0xc2132d05d31c914a87c6611c10748aeb04b58e8f';
+
+const token = (symbol: string, address: string, chainId: Hex): BridgeToken =>
+ ({
+ symbol,
+ address,
+ chainId,
+ decimals: 6,
+ }) as BridgeToken;
+
+const usdcBase = token('USDC', USDC_BASE, BASE);
+const ethBase = token('ETH', NATIVE_ADDRESS, BASE);
+const usdcPolygon = token('USDC', USDC_POLYGON, POLYGON);
+const usdtPolygon = token('USDT', USDT_POLYGON, POLYGON);
+const polPolygon = token('POL', NATIVE_ADDRESS, POLYGON);
+
+describe('selectDefaultReceiveToken', () => {
+ it('returns undefined when there are no options', () => {
+ expect(selectDefaultReceiveToken([], usdcBase)).toBeUndefined();
+ });
+
+ it('returns the first option when the sold token is unknown', () => {
+ const options = [usdcBase, ethBase];
+ expect(selectDefaultReceiveToken(options, undefined)).toBe(usdcBase);
+ });
+
+ it('defaults to the native token of the chain when selling a stablecoin', () => {
+ // Receive options are ordered stablecoins-first on the position chain, so a
+ // naive options[0] would return USDC — the very token being sold.
+ const options = [usdcBase, ethBase, usdcPolygon];
+ expect(selectDefaultReceiveToken(options, usdcBase)).toBe(ethBase);
+ });
+
+ it('matches the sold token case-insensitively before excluding it', () => {
+ const options = [usdcBase, ethBase];
+ const soldUpperCase = token('USDC', USDC_BASE.toUpperCase(), BASE);
+ expect(selectDefaultReceiveToken(options, soldUpperCase)).toBe(ethBase);
+ });
+
+ it('falls back to the first eligible candidate when selling the native token', () => {
+ // Selling ETH on Base: native preference would re-select ETH, so we fall
+ // through to the first non-ETH candidate.
+ const options = [ethBase, usdcBase];
+ expect(selectDefaultReceiveToken(options, ethBase)).toBe(usdcBase);
+ });
+
+ it('falls back to the first eligible candidate when the native token is not an option', () => {
+ // No native candidate present for the sold token's chain.
+ const options = [usdcPolygon, usdtPolygon];
+ expect(selectDefaultReceiveToken(options, usdcPolygon)).toBe(usdtPolygon);
+ });
+
+ it('only prefers the native token on the sold token chain', () => {
+ // Native ETH/Base must not be chosen when selling on Polygon — the native
+ // preference is scoped to the sold token's own chain.
+ const options = [usdcPolygon, polPolygon, ethBase];
+ expect(selectDefaultReceiveToken(options, usdcPolygon)).toBe(polPolygon);
+ });
+
+ it('returns the first option when every candidate is the sold token', () => {
+ const options = [usdcBase];
+ expect(selectDefaultReceiveToken(options, usdcBase)).toBe(usdcBase);
+ });
+});
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.ts
new file mode 100644
index 00000000000..02c4ce5c40b
--- /dev/null
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/utils/selectDefaultReceiveToken.ts
@@ -0,0 +1,48 @@
+import type { BridgeToken } from '../../../../../../UI/Bridge/types';
+import { getNativeSourceToken } from '../../../../../../UI/Bridge/utils/tokenUtils';
+import { getTokenKey } from '../tokenKey';
+
+/**
+ * Picks the default "Receive" token for QuickBuy Sell mode.
+ *
+ * The receive options are ordered stablecoins-first on the position's chain, so
+ * naively taking the first entry can default to the *same* token the user is
+ * selling (e.g. selling USDC on Base would default to receiving USDC, which is a
+ * no-op). To keep the default sensible this excludes the token being sold and
+ * then prefers the native token of the sold token's chain (e.g. ETH on Base) so
+ * a sale defaults to cashing out into the chain's native asset. When the user is
+ * already selling the native token, or no native option exists, it falls back to
+ * the first remaining candidate (a stablecoin on the position chain).
+ *
+ * @param options - The ordered receive-token candidates.
+ * @param soldToken - The token being sold (Sell mode source), if known.
+ * @returns The token to preselect, or `undefined` when there are no options.
+ */
+export const selectDefaultReceiveToken = (
+ options: BridgeToken[],
+ soldToken: Pick | undefined,
+): BridgeToken | undefined => {
+ if (options.length === 0) return undefined;
+ if (!soldToken) return options[0];
+
+ const soldKey = getTokenKey(soldToken);
+ const eligible = options.filter((token) => getTokenKey(token) !== soldKey);
+ // Every option matched the sold token (shouldn't happen in practice) — fall
+ // back to the first so we always return something selectable.
+ if (eligible.length === 0) return options[0];
+
+ let nativeKey: string | undefined;
+ try {
+ nativeKey = getTokenKey(getNativeSourceToken(soldToken.chainId));
+ } catch {
+ // Native asset can't be resolved for this chain — skip the native
+ // preference and fall through to the first eligible candidate.
+ }
+
+ if (nativeKey && nativeKey !== soldKey) {
+ const native = eligible.find((token) => getTokenKey(token) === nativeKey);
+ if (native) return native;
+ }
+
+ return eligible[0];
+};
diff --git a/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.test.tsx b/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.test.tsx
new file mode 100644
index 00000000000..0fe271638b1
--- /dev/null
+++ b/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.test.tsx
@@ -0,0 +1,82 @@
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { strings } from '../../../../../../../locales/i18n';
+import { ARBITRUM_USDC, PERPS_CURRENCY } from '../../../constants/perps';
+import { useAddToken } from '../../../hooks/tokens/useAddToken';
+import useNavbar from '../../../hooks/ui/useNavbar';
+import { useMoneyAccountPaymentOverride } from '../../../hooks/pay/useMoneyAccountPaymentOverride';
+import { useParams } from '../../../../../../util/navigation/navUtils';
+import { PayWithOption } from '../../confirm/confirm-component';
+import { CustomAmountInfo } from '../custom-amount-info';
+import { PerpsDepositInfo } from './perps-deposit-info';
+
+jest.mock('../../../hooks/ui/useNavbar');
+jest.mock('../../../hooks/tokens/useAddToken');
+jest.mock('../../../hooks/pay/useMoneyAccountPaymentOverride');
+jest.mock('../../../../../../util/navigation/navUtils', () => ({
+ ...jest.requireActual('../../../../../../util/navigation/navUtils'),
+ useParams: jest.fn(),
+}));
+jest.mock('../custom-amount-info', () => ({
+ CustomAmountInfo: jest.fn(() => null),
+}));
+
+describe('PerpsDepositInfo', () => {
+ const mockUseNavbar = jest.mocked(useNavbar);
+ const mockUseAddToken = jest.mocked(useAddToken);
+ const mockUseParams = jest.mocked(useParams);
+ const mockCustomAmountInfo = jest.mocked(CustomAmountInfo);
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseParams.mockReturnValue({});
+ });
+
+ it('sets navbar title to default perps deposit title', () => {
+ render();
+
+ expect(mockUseNavbar).toHaveBeenCalledWith(
+ strings('confirm.title.perps_deposit'),
+ );
+ });
+
+ it('sets navbar title to "Transfer to Perps" when payWithOption is MoneyAccount', () => {
+ mockUseParams.mockReturnValue({
+ payWithOption: PayWithOption.MoneyAccount,
+ });
+
+ render();
+
+ expect(mockUseNavbar).toHaveBeenCalledWith(
+ strings('perps.transfer_to_perps'),
+ );
+ });
+
+ it('registers Arbitrum USDC token', () => {
+ render();
+
+ expect(mockUseAddToken).toHaveBeenCalledWith({
+ chainId: CHAIN_IDS.ARBITRUM,
+ decimals: ARBITRUM_USDC.decimals,
+ name: ARBITRUM_USDC.name,
+ symbol: ARBITRUM_USDC.symbol,
+ tokenAddress: ARBITRUM_USDC.address,
+ });
+ });
+
+ it('renders CustomAmountInfo with perps currency and hasMax', () => {
+ render();
+
+ expect(mockCustomAmountInfo).toHaveBeenCalledWith(
+ expect.objectContaining({ currency: PERPS_CURRENCY, hasMax: true }),
+ undefined,
+ );
+ });
+
+ it('calls useMoneyAccountPaymentOverride', () => {
+ render();
+
+ expect(useMoneyAccountPaymentOverride).toHaveBeenCalled();
+ });
+});
diff --git a/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.tsx b/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.tsx
index 05f780e0c14..d663b8fd1c0 100644
--- a/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.tsx
+++ b/app/components/Views/confirmations/components/info/perps-deposit-info/perps-deposit-info.tsx
@@ -6,9 +6,20 @@ import { ARBITRUM_USDC, PERPS_CURRENCY } from '../../../constants/perps';
import { useAddToken } from '../../../hooks/tokens/useAddToken';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { useMoneyAccountPaymentOverride } from '../../../hooks/pay/useMoneyAccountPaymentOverride';
+import { useParams } from '../../../../../../util/navigation/navUtils';
+import {
+ ConfirmationParams,
+ PayWithOption,
+} from '../../confirm/confirm-component';
export function PerpsDepositInfo() {
- useNavbar(strings('confirm.title.perps_deposit'));
+ const { payWithOption } = useParams({});
+ const title =
+ payWithOption === PayWithOption.MoneyAccount
+ ? strings('perps.transfer_to_perps')
+ : strings('confirm.title.perps_deposit');
+
+ useNavbar(title);
useMoneyAccountPaymentOverride();
useAddToken({
diff --git a/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.test.tsx b/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.test.tsx
new file mode 100644
index 00000000000..9c4f1f15ba9
--- /dev/null
+++ b/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.test.tsx
@@ -0,0 +1,82 @@
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { strings } from '../../../../../../../locales/i18n';
+import { POLYGON_PUSD, PREDICT_CURRENCY } from '../../../constants/predict';
+import { useAddToken } from '../../../hooks/tokens/useAddToken';
+import useNavbar from '../../../hooks/ui/useNavbar';
+import { useMoneyAccountPaymentOverride } from '../../../hooks/pay/useMoneyAccountPaymentOverride';
+import { useParams } from '../../../../../../util/navigation/navUtils';
+import { PayWithOption } from '../../confirm/confirm-component';
+import { CustomAmountInfo } from '../custom-amount-info';
+import { PredictDepositInfo } from './predict-deposit-info';
+
+jest.mock('../../../hooks/ui/useNavbar');
+jest.mock('../../../hooks/tokens/useAddToken');
+jest.mock('../../../hooks/pay/useMoneyAccountPaymentOverride');
+jest.mock('../../../../../../util/navigation/navUtils', () => ({
+ ...jest.requireActual('../../../../../../util/navigation/navUtils'),
+ useParams: jest.fn(),
+}));
+jest.mock('../custom-amount-info', () => ({
+ CustomAmountInfo: jest.fn(() => null),
+}));
+
+describe('PredictDepositInfo', () => {
+ const mockUseNavbar = jest.mocked(useNavbar);
+ const mockUseAddToken = jest.mocked(useAddToken);
+ const mockUseParams = jest.mocked(useParams);
+ const mockCustomAmountInfo = jest.mocked(CustomAmountInfo);
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseParams.mockReturnValue({});
+ });
+
+ it('sets navbar title to default predict deposit title', () => {
+ render();
+
+ expect(mockUseNavbar).toHaveBeenCalledWith(
+ strings('confirm.title.predict_deposit'),
+ );
+ });
+
+ it('sets navbar title to "Transfer to Predictions" when payWithOption is MoneyAccount', () => {
+ mockUseParams.mockReturnValue({
+ payWithOption: PayWithOption.MoneyAccount,
+ });
+
+ render();
+
+ expect(mockUseNavbar).toHaveBeenCalledWith(
+ strings('predict.transfer_to_predictions'),
+ );
+ });
+
+ it('registers Polygon pUSD token', () => {
+ render();
+
+ expect(mockUseAddToken).toHaveBeenCalledWith({
+ chainId: CHAIN_IDS.POLYGON,
+ decimals: POLYGON_PUSD.decimals,
+ name: POLYGON_PUSD.name,
+ symbol: POLYGON_PUSD.symbol,
+ tokenAddress: POLYGON_PUSD.address,
+ });
+ });
+
+ it('renders CustomAmountInfo with predict currency', () => {
+ render();
+
+ expect(mockCustomAmountInfo).toHaveBeenCalledWith(
+ expect.objectContaining({ currency: PREDICT_CURRENCY }),
+ undefined,
+ );
+ });
+
+ it('calls useMoneyAccountPaymentOverride', () => {
+ render();
+
+ expect(useMoneyAccountPaymentOverride).toHaveBeenCalled();
+ });
+});
diff --git a/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.tsx b/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.tsx
index ab82debcdc6..f4ba844b15e 100644
--- a/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.tsx
+++ b/app/components/Views/confirmations/components/info/predict-deposit-info/predict-deposit-info.tsx
@@ -6,9 +6,20 @@ import { POLYGON_PUSD, PREDICT_CURRENCY } from '../../../constants/predict';
import { useAddToken } from '../../../hooks/tokens/useAddToken';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { useMoneyAccountPaymentOverride } from '../../../hooks/pay/useMoneyAccountPaymentOverride';
+import { useParams } from '../../../../../../util/navigation/navUtils';
+import {
+ ConfirmationParams,
+ PayWithOption,
+} from '../../confirm/confirm-component';
export function PredictDepositInfo() {
- useNavbar(strings('confirm.title.predict_deposit'));
+ const { payWithOption } = useParams({});
+ const title =
+ payWithOption === PayWithOption.MoneyAccount
+ ? strings('predict.transfer_to_predictions')
+ : strings('confirm.title.predict_deposit');
+
+ useNavbar(title);
useMoneyAccountPaymentOverride();
useAddToken({
diff --git a/app/constants/transaction.ts b/app/constants/transaction.ts
index b691c760456..9df808c1a7c 100644
--- a/app/constants/transaction.ts
+++ b/app/constants/transaction.ts
@@ -27,7 +27,7 @@ export const INTERNAL_ORIGINS = [
TransactionTypes.MMM,
TransactionTypes.MMM_CARD,
ORIGIN_METAMASK,
-];
+].filter(Boolean);
export enum EIP5792ErrorCode {
UnsupportedNonOptionalCapability = 5700,
diff --git a/app/contexts/route-messenger.ts b/app/contexts/route-messenger.ts
new file mode 100644
index 00000000000..796fdf0efc8
--- /dev/null
+++ b/app/contexts/route-messenger.ts
@@ -0,0 +1,28 @@
+import { createContext, useContext } from 'react';
+
+import { RouteMessenger } from '../messengers/route-messenger';
+
+/**
+ * Context that holds the messenger for the current route.
+ *
+ * @see {@link RouteWithMessenger}
+ */
+export const RouteMessengerContext = createContext(null);
+
+/**
+ * Hook to access the messenger for the current route from context.
+ *
+ * @returns The route messenger in context.
+ * @throws If the route messenger has not been set.
+ */
+export function useRouteMessenger(): RouteMessenger {
+ const messenger = useContext(RouteMessengerContext);
+
+ if (!messenger) {
+ throw new Error(
+ 'useRouteMessenger must be used within a route messenger context.',
+ );
+ }
+
+ return messenger;
+}
diff --git a/app/contexts/ui-messenger.tsx b/app/contexts/ui-messenger.tsx
new file mode 100644
index 00000000000..ba156d48f05
--- /dev/null
+++ b/app/contexts/ui-messenger.tsx
@@ -0,0 +1,48 @@
+import React, { createContext, ReactNode, useContext } from 'react';
+import { UIMessenger } from '../messengers/ui-messenger';
+
+/**
+ * Context that holds the UI messenger.
+ */
+export const UIMessengerContext = createContext(null);
+
+/**
+ * Provides the UI messenger to child components via context.
+ *
+ * @param args - The arguments to this function.
+ * @param args.value - The UI messenger to load into context.
+ * @param args.children - The components to wrap.
+ */
+export const UIMessengerProvider = ({
+ value,
+ children,
+}: {
+ value: UIMessenger;
+ children: ReactNode;
+}) => (
+
+ {children}
+
+);
+
+/**
+ * Hook to access the UI messenger from context.
+ *
+ * Used to derive route-level messengers. Do not use this hook directly,
+ * only use it to define route-level messenger hooks.
+ *
+ * @returns The UI messenger in context.
+ * @throws If the UI messenger is not available (e.g., hook is used outside of
+ * the provider).
+ */
+export function useUIMessenger(): UIMessenger {
+ const messenger = useContext(UIMessengerContext);
+
+ if (!messenger) {
+ throw new Error(
+ 'The `useUIMessenger` hook must be used within a `UIMessengerProvider`.',
+ );
+ }
+
+ return messenger;
+}
diff --git a/app/core/AgenticCli/AgenticCliMwpConnectionService.test.ts b/app/core/AgenticCli/AgenticCliMwpConnectionService.test.ts
index 58f9986da76..79432cdf1c5 100644
--- a/app/core/AgenticCli/AgenticCliMwpConnectionService.test.ts
+++ b/app/core/AgenticCli/AgenticCliMwpConnectionService.test.ts
@@ -1,4 +1,7 @@
-import { HostApplicationAdapter } from '../SDKConnectV2/adapters/host-application-adapter';
+import {
+ AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS,
+ HostApplicationAdapter,
+} from '../SDKConnectV2/adapters/host-application-adapter';
import { KeyManager } from '../SDKConnectV2/services/key-manager';
import { Connection } from '../SDKConnectV2/services/connection';
import { ConnectionInfo } from '../SDKConnectV2/types/connection-info';
@@ -11,10 +14,6 @@ import {
handleAgenticCliConnectDeeplink,
isAgenticCliDeeplink,
} from './AgenticCliMwpConnectionService';
-import {
- hideAgenticCliConnectionLoading,
- showAgenticCliConnectionLoading,
-} from './agenticCliLoading';
import {
hideAgenticCliOtpCode,
showAgenticCliOtpCode,
@@ -24,10 +23,6 @@ import Engine from '../Engine';
jest.mock('../SDKConnectV2/adapters/host-application-adapter');
jest.mock('../SDKConnectV2/services/key-manager');
jest.mock('../SDKConnectV2/services/connection');
-jest.mock('./agenticCliLoading', () => ({
- showAgenticCliConnectionLoading: jest.fn(),
- hideAgenticCliConnectionLoading: jest.fn(),
-}));
jest.mock('./agenticCliOtpUi', () => ({
showAgenticCliOtpCode: jest.fn(),
hideAgenticCliOtpCode: jest.fn(),
@@ -201,7 +196,7 @@ describe('AgenticCliMwpConnectionService', () => {
await Promise.resolve();
expect(Connection.create).not.toHaveBeenCalled();
- expect(showAgenticCliConnectionLoading).not.toHaveBeenCalled();
+ expect(mockHostApp.showConnectionLoading).not.toHaveBeenCalled();
resolveUnlock();
await promise;
@@ -217,7 +212,7 @@ describe('AgenticCliMwpConnectionService', () => {
conn: mockConnection,
}),
);
- expect(hideAgenticCliConnectionLoading).toHaveBeenCalledWith(
+ expect(mockHostApp.hideConnectionLoading).toHaveBeenCalledWith(
expect.objectContaining({
id: mockConnectionInfo.id,
}),
@@ -246,7 +241,7 @@ describe('AgenticCliMwpConnectionService', () => {
expect(hideAgenticCliOtpCode).toHaveBeenCalledWith(
expect.objectContaining({ id: mockConnectionInfo.id }),
);
- expect(hideAgenticCliConnectionLoading).toHaveBeenCalledWith(
+ expect(mockHostApp.hideConnectionLoading).toHaveBeenCalledWith(
expect.objectContaining({ id: mockConnectionInfo.id }),
);
expect(mockHostApp.showConnectionError).toHaveBeenCalled();
@@ -295,8 +290,12 @@ describe('AgenticCliMwpConnectionService', () => {
cleanupConnection: jest.fn().mockResolvedValue(undefined),
});
- expect(showAgenticCliConnectionLoading).toHaveBeenCalledTimes(1);
- expect(hideAgenticCliConnectionLoading).toHaveBeenCalledTimes(1);
+ expect(mockHostApp.showConnectionLoading).toHaveBeenCalledTimes(1);
+ expect(mockHostApp.showConnectionLoading).toHaveBeenCalledWith(
+ expect.objectContaining({ id: mockConnectionInfo.id }),
+ { autodismissMs: AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS },
+ );
+ expect(mockHostApp.hideConnectionLoading).toHaveBeenCalledTimes(1);
expect(mockHandleAgenticCliQrLogin).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/core/AgenticCli/AgenticCliMwpConnectionService.ts b/app/core/AgenticCli/AgenticCliMwpConnectionService.ts
index 53de2961a8e..1d88a7f3eee 100644
--- a/app/core/AgenticCli/AgenticCliMwpConnectionService.ts
+++ b/app/core/AgenticCli/AgenticCliMwpConnectionService.ts
@@ -12,11 +12,8 @@ import { Connection } from '../SDKConnectV2/services/connection';
import logger, { redactUrl } from '../SDKConnectV2/services/logger';
import { ConnectionInfo } from '../SDKConnectV2/types/connection-info';
import { IHostApplicationAdapter } from '../SDKConnectV2/types/host-application-adapter';
+import { AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS } from '../SDKConnectV2/adapters/host-application-adapter';
import { MetaMetricsEvents } from '../Analytics/MetaMetrics.events';
-import {
- hideAgenticCliConnectionLoading,
- showAgenticCliConnectionLoading,
-} from './agenticCliLoading';
import {
hideAgenticCliOtpCode,
showAgenticCliOtpCode,
@@ -139,7 +136,9 @@ export async function handleAgenticCliConnectDeeplink(
}
// --- Create MWP connection and connect (untrusted) ---
- showAgenticCliConnectionLoading(connInfo);
+ deps.hostapp.showConnectionLoading(connInfo, {
+ autodismissMs: AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS,
+ });
agenticCliStage = 'create-mwp-connection';
conn = await Connection.create(
connInfo,
@@ -221,7 +220,7 @@ export async function handleAgenticCliConnectDeeplink(
}
} finally {
if (connInfo) {
- hideAgenticCliConnectionLoading(connInfo);
+ deps.hostapp.hideConnectionLoading(connInfo);
hideAgenticCliOtpCode(connInfo);
}
}
diff --git a/app/core/AgenticCli/agenticCliLoading.ts b/app/core/AgenticCli/agenticCliLoading.ts
deleted file mode 100644
index 19370c34d58..00000000000
--- a/app/core/AgenticCli/agenticCliLoading.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { store } from '../../store';
-import {
- hideNotificationById,
- showSimpleNotification,
-} from '../../actions/notification';
-import { strings } from '../../../locales/i18n';
-import { ConnectionInfo } from '../SDKConnectV2/types/connection-info';
-
-const AGENTIC_CLI_LOADING_AUTODISMISS_MS = 15_000;
-
-export const showAgenticCliConnectionLoading = (
- conninfo: ConnectionInfo,
-): void => {
- store.dispatch(
- showSimpleNotification({
- id: conninfo.id,
- autodismiss: AGENTIC_CLI_LOADING_AUTODISMISS_MS,
- title: strings('sdk_connect_v2.show_loading.title'),
- description: strings('sdk_connect_v2.show_loading.description', {
- dappName: conninfo.metadata.dapp.name,
- }),
- status: 'pending',
- }),
- );
-};
-
-export const hideAgenticCliConnectionLoading = (
- conninfo: ConnectionInfo,
-): void => {
- store.dispatch(hideNotificationById(conninfo.id));
-};
diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts
index 2d7669358ba..816722a2ce3 100644
--- a/app/core/Analytics/MetaMetrics.events.ts
+++ b/app/core/Analytics/MetaMetrics.events.ts
@@ -680,6 +680,7 @@ enum EVENT_NAME {
PREDICT_GEO_BLOCKED_TRIGGERED = 'Geo Blocked Triggered',
PREDICT_FEED_VIEWED = 'Predict Feed Viewed',
PREDICT_BANNER_ACTION = 'Predict Banner Action',
+ PREDICT_SEARCH_INTERACTED = 'Predict Search Interacted',
// Trending
TRENDING_FEED_VIEWED = 'Trending Feed Viewed',
@@ -1893,6 +1894,7 @@ const events = {
),
PREDICT_FEED_VIEWED: generateOpt(EVENT_NAME.PREDICT_FEED_VIEWED),
PREDICT_BANNER_ACTION: generateOpt(EVENT_NAME.PREDICT_BANNER_ACTION),
+ PREDICT_SEARCH_INTERACTED: generateOpt(EVENT_NAME.PREDICT_SEARCH_INTERACTED),
TRENDING_FEED_VIEWED: generateOpt(EVENT_NAME.TRENDING_FEED_VIEWED),
diff --git a/app/core/BackgroundBridge/BackgroundBridge.js b/app/core/BackgroundBridge/BackgroundBridge.js
index aecff35e386..17a8a38c6cf 100644
--- a/app/core/BackgroundBridge/BackgroundBridge.js
+++ b/app/core/BackgroundBridge/BackgroundBridge.js
@@ -870,13 +870,6 @@ export class BackgroundBridge extends EventEmitter {
*/
createEip5792Middleware() {
return createEip5792Middleware({
- getAccounts: () => {
- const { AccountsController } = Engine.context;
- const addresses = AccountsController.listAccounts().map(
- (acc) => acc.address,
- );
- return Promise.resolve(addresses);
- },
// EIP-5792
processSendCalls: processSendCalls.bind(
null,
@@ -915,6 +908,8 @@ export class BackgroundBridge extends EventEmitter {
}),
isAuxiliaryFundsSupported: (chainId) =>
ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId),
+ getPermittedAccountsForOrigin: async () =>
+ getPermittedAccounts(this.channelIdOrOrigin),
},
Engine.controllerMessenger,
),
diff --git a/app/core/Engine/Engine.test.ts b/app/core/Engine/Engine.test.ts
index 788da2e0fb7..5302792ec93 100644
--- a/app/core/Engine/Engine.test.ts
+++ b/app/core/Engine/Engine.test.ts
@@ -322,42 +322,29 @@ describe('Engine', () => {
);
});
- it('getSnapKeyring gets or creates a snap keyring', async () => {
+ it('getSnapKeyring delegates to SnapAccountService.getLegacySnapKeyring', async () => {
const engine = new EngineClass(TEST_ANALYTICS_ID, backgroundState);
const mockSnapKeyring = { type: 'Snap Keyring' } as unknown as SnapKeyring;
- jest
- .spyOn(engine.keyringController, 'getKeyringsByType')
- .mockImplementation(() => [mockSnapKeyring]);
- const getSnapKeyringSpy = jest
- .spyOn(engine, 'getSnapKeyring')
- .mockImplementation(async () => mockSnapKeyring);
+ jest
+ .spyOn(engine.context.SnapAccountService, 'getLegacySnapKeyring')
+ .mockResolvedValue(mockSnapKeyring as never);
const result = await engine.getSnapKeyring();
- expect(getSnapKeyringSpy).toHaveBeenCalled();
+
expect(result).toEqual(mockSnapKeyring);
});
- it('getSnapKeyring creates a new snap keyring if none exists', async () => {
+ it('getSnapKeyring propagates errors from SnapAccountService.getLegacySnapKeyring', async () => {
const engine = new EngineClass(TEST_ANALYTICS_ID, backgroundState);
- const mockSnapKeyring = { type: 'Snap Keyring' } as unknown as SnapKeyring;
-
- jest
- .spyOn(engine.keyringController, 'getKeyringsByType')
- .mockImplementationOnce(() => [])
- .mockImplementationOnce(() => [mockSnapKeyring]);
jest
- .spyOn(engine.keyringController, 'addNewKeyring')
- .mockResolvedValue({ id: '1234', name: 'Snap Keyring' });
-
- const getSnapKeyringSpy = jest
- .spyOn(engine, 'getSnapKeyring')
- .mockImplementation(async () => mockSnapKeyring);
+ .spyOn(engine.context.SnapAccountService, 'getLegacySnapKeyring')
+ .mockRejectedValue(new Error('keyring unavailable'));
- const result = await engine.getSnapKeyring();
- expect(getSnapKeyringSpy).toHaveBeenCalled();
- expect(result).toEqual(mockSnapKeyring);
+ await expect(engine.getSnapKeyring()).rejects.toThrow(
+ 'keyring unavailable',
+ );
});
it('enables the RPC failover feature if the walletFrameworkRpcFailoverEnabled feature flag is already enabled', () => {
diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts
index aaf76a062cd..6fac2725d91 100644
--- a/app/core/Engine/Engine.ts
+++ b/app/core/Engine/Engine.ts
@@ -17,9 +17,6 @@ import { AccountsController } from '@metamask/accounts-controller';
import {
KeyringController,
KeyringControllerState,
- ///: BEGIN:ONLY_INCLUDE_IF(snaps)
- KeyringTypes,
- ///: END:ONLY_INCLUDE_IF
} from '@metamask/keyring-controller';
import { NetworkState, NetworkStatus } from '@metamask/network-controller';
import {
@@ -36,10 +33,6 @@ import {
SubjectMetadataController,
///: END:ONLY_INCLUDE_IF
} from '@metamask/permission-controller';
-import {
- QrKeyring,
- QrKeyringDeferredPromiseBridge,
-} from '@metamask/eth-qr-keyring';
import { isTestNet } from '../../util/networks';
import { deprecatedGetNetworkId } from '../../util/networks/engineNetworkUtils';
import AppConstants from '../AppConstants';
@@ -102,6 +95,7 @@ import { multichainAssetsControllerInit } from './controllers/multichain-assets-
import { multichainAssetsRatesControllerInit } from './controllers/multichain-assets-rates-controller/multichain-assets-rates-controller-init';
import { multichainTransactionsControllerInit } from './controllers/multichain-transactions-controller/multichain-transactions-controller-init';
import { multichainAccountServiceInit } from './controllers/multichain-account-service/multichain-account-service-init';
+import { snapAccountServiceInit } from './controllers/snap-account-service/snap-account-service-init';
import { SnapKeyring } from '@metamask/eth-snap-keyring';
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(snaps)
@@ -378,6 +372,7 @@ export class Engine {
AccountActivityService: accountActivityServiceInit,
OHLCVService: ohlcvServiceInit,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+ SnapAccountService: snapAccountServiceInit,
MultichainAssetsController: multichainAssetsControllerInit,
MultichainAssetsRatesController: multichainAssetsRatesControllerInit,
MultichainBalancesController: multichainBalancesControllerInit,
@@ -544,6 +539,7 @@ export class Engine {
messengerClientsByName.MultichainTransactionsController;
const multichainAccountService =
messengerClientsByName.MultichainAccountService;
+ const snapAccountService = messengerClientsByName.SnapAccountService;
///: END:ONLY_INCLUDE_IF
const networkEnablementController =
@@ -617,6 +613,7 @@ export class Engine {
MultichainAssetsRatesController: multichainAssetsRatesController,
MultichainTransactionsController: multichainTransactionsController,
MultichainAccountService: multichainAccountService,
+ SnapAccountService: snapAccountService,
///: END:ONLY_INCLUDE_IF
TokenSearchDiscoveryDataController: tokenSearchDiscoveryDataController,
MultichainNetworkController: multichainNetworkController,
@@ -1150,18 +1147,8 @@ export class Engine {
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
getSnapKeyring = async (): Promise => {
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- let [snapKeyring] = this.keyringController.getKeyringsByType(
- KeyringTypes.snap,
- );
- if (!snapKeyring) {
- await this.keyringController.addNewKeyring(KeyringTypes.snap);
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- [snapKeyring] = this.keyringController.getKeyringsByType(
- KeyringTypes.snap,
- );
- }
- return snapKeyring as SnapKeyring;
+ const { SnapAccountService } = this.context;
+ return await SnapAccountService.getLegacySnapKeyring();
};
/**
diff --git a/app/core/Engine/constants.ts b/app/core/Engine/constants.ts
index 5895151d60d..836927f2047 100644
--- a/app/core/Engine/constants.ts
+++ b/app/core/Engine/constants.ts
@@ -16,6 +16,7 @@ export const STATELESS_NON_CONTROLLER_NAMES = [
'AccountActivityService',
'OHLCVService',
'MultichainAccountService',
+ 'SnapAccountService',
'GeolocationApiService',
'ProfileMetricsService',
'RampsService',
diff --git a/app/core/Engine/controllers/multichain-routing-service-init.ts b/app/core/Engine/controllers/multichain-routing-service-init.ts
index 5767e64e8ee..19e97a7c20f 100644
--- a/app/core/Engine/controllers/multichain-routing-service-init.ts
+++ b/app/core/Engine/controllers/multichain-routing-service-init.ts
@@ -5,7 +5,6 @@ import {
} from '@metamask/snaps-controllers';
import { MultichainRoutingServiceInitMessenger } from '../messengers/multichain-routing-service-messenger';
import { SnapKeyring } from '@metamask/eth-snap-keyring';
-import { KeyringTypes } from '@metamask/keyring-controller';
/**
* Initialize the multichain routing service.
@@ -19,44 +18,18 @@ export const multichainRoutingServiceInit: MessengerClientInitFunction<
MultichainRoutingServiceMessenger,
MultichainRoutingServiceInitMessenger
> = ({ controllerMessenger, initMessenger }) => {
- const getSnapKeyring = async (): Promise => {
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- let [snapKeyring] = initMessenger.call(
- 'KeyringController:getKeyringsByType',
- KeyringTypes.snap,
- );
-
- if (!snapKeyring) {
- await initMessenger.call(
- 'KeyringController:addNewKeyring',
- KeyringTypes.snap,
- );
-
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- [snapKeyring] = initMessenger.call(
- 'KeyringController:getKeyringsByType',
- KeyringTypes.snap,
- );
- }
- return snapKeyring as SnapKeyring;
- };
-
- // This fixes an issue where `withKeyring` would lock the `KeyringController`
- // mutex. That meant that if a snap requested a keyring operation (like
- // requesting entropy) while the `KeyringController` was locked, it would
- // cause a deadlock. This is a temporary fix until we can refactor how we
- // handle requests to the Snaps Keyring.
- const withSnapKeyring = async (
- operation: ({ keyring }: { keyring: unknown }) => void,
+ const withSnapKeyring = async (
+ operation: ({ keyring }: { keyring: SnapKeyring }) => Promise,
) => {
- const keyring = await getSnapKeyring();
+ const keyring = await initMessenger.call(
+ 'SnapAccountService:getLegacySnapKeyring',
+ );
return operation({ keyring });
};
const controller = new MultichainRoutingService({
messenger: controllerMessenger,
- // @ts-expect-error: Type for `withSnapKeyring` is different.
withSnapKeyring,
});
diff --git a/app/core/Engine/controllers/snap-account-service/snap-account-service-init.test.ts b/app/core/Engine/controllers/snap-account-service/snap-account-service-init.test.ts
new file mode 100644
index 00000000000..78f4eb4de77
--- /dev/null
+++ b/app/core/Engine/controllers/snap-account-service/snap-account-service-init.test.ts
@@ -0,0 +1,54 @@
+import { SnapAccountService } from '@metamask/snap-account-service';
+import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger';
+import { ExtendedMessenger } from '../../../ExtendedMessenger';
+import { buildMessengerClientInitRequestMock } from '../../utils/test-utils';
+import { getSnapAccountServiceMessenger } from '../../messengers/snap-account-service-messenger/snap-account-service-messenger';
+import { snapAccountServiceInit } from './snap-account-service-init';
+
+jest.mock('@metamask/snap-account-service');
+jest.mock('../../utils/ensureOnboardingComplete', () => ({
+ ensureOnboardingComplete: jest.fn(() => jest.fn()),
+}));
+
+function getInitRequestMock() {
+ const baseMessenger = new ExtendedMessenger({
+ namespace: MOCK_ANY_NAMESPACE,
+ });
+
+ return {
+ ...buildMessengerClientInitRequestMock(baseMessenger),
+ controllerMessenger: getSnapAccountServiceMessenger(baseMessenger),
+ initMessenger: undefined,
+ };
+}
+
+describe('snapAccountServiceInit', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('constructs a SnapAccountService with the controller messenger', () => {
+ const requestMock = getInitRequestMock();
+
+ snapAccountServiceInit(requestMock);
+
+ expect(SnapAccountService).toHaveBeenCalledTimes(1);
+ expect(SnapAccountService).toHaveBeenCalledWith({
+ messenger: requestMock.controllerMessenger,
+ config: {
+ snapPlatformWatcher: {
+ ensureOnboardingComplete: expect.any(Function),
+ },
+ },
+ });
+ });
+
+ it('returns the constructed service as the controller', () => {
+ const requestMock = getInitRequestMock();
+
+ const result = snapAccountServiceInit(requestMock);
+
+ const constructed = jest.mocked(SnapAccountService).mock.instances[0];
+ expect(result.controller).toBe(constructed);
+ });
+});
diff --git a/app/core/Engine/controllers/snap-account-service/snap-account-service-init.ts b/app/core/Engine/controllers/snap-account-service/snap-account-service-init.ts
new file mode 100644
index 00000000000..877195ac805
--- /dev/null
+++ b/app/core/Engine/controllers/snap-account-service/snap-account-service-init.ts
@@ -0,0 +1,27 @@
+import { SnapAccountService } from '@metamask/snap-account-service';
+import type { MessengerClientInitFunction } from '../../types';
+import type { SnapAccountServiceMessenger } from '../../messengers/snap-account-service-messenger/snap-account-service-messenger';
+import { ensureOnboardingComplete } from '../../utils/ensureOnboardingComplete';
+
+/**
+ * Initialize the Snap account service.
+ *
+ * @param request - The request object.
+ * @param request.controllerMessenger - The messenger to use for the service.
+ * @returns The initialized service.
+ */
+export const snapAccountServiceInit: MessengerClientInitFunction<
+ SnapAccountService,
+ SnapAccountServiceMessenger
+> = ({ controllerMessenger }) => {
+ const controller = new SnapAccountService({
+ messenger: controllerMessenger,
+ config: {
+ snapPlatformWatcher: {
+ ensureOnboardingComplete,
+ },
+ },
+ });
+
+ return { controller };
+};
diff --git a/app/core/Engine/controllers/snaps/snap-controller-init.ts b/app/core/Engine/controllers/snaps/snap-controller-init.ts
index a0078970a21..001f2e02602 100644
--- a/app/core/Engine/controllers/snaps/snap-controller-init.ts
+++ b/app/core/Engine/controllers/snaps/snap-controller-init.ts
@@ -18,18 +18,12 @@ import {
pbkdf2,
} from '../../../Encryptor';
import { selectBasicFunctionalityEnabled } from '../../../../selectors/settings';
-import { store, runSaga } from '../../../../store';
+import { store } from '../../../../store';
import PREINSTALLED_SNAPS from '../../../../lib/snaps/preinstalled-snaps';
import { buildAndTrackEvent } from '../../utils/analytics';
import type { AnalyticsUnfilteredProperties } from '../../../../util/analytics/analytics.types';
-import { take } from 'redux-saga/effects';
-import { selectCompletedOnboarding } from '../../../../selectors/onboarding';
-import {
- SET_COMPLETED_ONBOARDING,
- SetCompletedOnboardingAction,
-} from '../../../../actions/onboarding';
-import { SagaIterator } from 'redux-saga';
import { getMnemonicSeed } from '../../../Snaps/permissions/utils';
+import { ensureOnboardingComplete } from '../../utils/ensureOnboardingComplete';
import { CAN_INSTALL_THIRD_PARTY_SNAPS } from '../../../../constants/snaps';
/**
@@ -76,33 +70,6 @@ export const snapControllerInit: MessengerClientInitFunction<
};
}
- function* ensureOnboardingCompleteSaga(): SagaIterator {
- while (true) {
- const result = (yield take([
- SET_COMPLETED_ONBOARDING,
- ])) as SetCompletedOnboardingAction;
-
- if (result.completedOnboarding) {
- return;
- }
- }
- }
-
- let onboardingPromise: Promise | null = null;
-
- async function ensureOnboardingComplete() {
- if (selectCompletedOnboarding(store.getState())) {
- return;
- }
-
- if (!onboardingPromise) {
- onboardingPromise = runSaga(ensureOnboardingCompleteSaga).toPromise();
- }
-
- await onboardingPromise;
- onboardingPromise = null;
- }
-
const controller = new SnapController({
environmentEndowmentPermissions: Object.values(EndowmentPermissions),
excludedPermissions: {
diff --git a/app/core/Engine/messengers/accounts-controller-messenger/index.ts b/app/core/Engine/messengers/accounts-controller-messenger/index.ts
index 25dabc7df88..6a66595ea92 100644
--- a/app/core/Engine/messengers/accounts-controller-messenger/index.ts
+++ b/app/core/Engine/messengers/accounts-controller-messenger/index.ts
@@ -1,10 +1,5 @@
import { AccountsControllerMessenger } from '@metamask/accounts-controller';
import { RootExtendedMessenger, RootMessenger } from '../../types';
-import {
- SnapKeyringAccountAssetListUpdatedEvent,
- SnapKeyringAccountBalancesUpdatedEvent,
- SnapKeyringAccountTransactionsUpdatedEvent,
-} from '../../../SnapKeyring/constants';
import {
Messenger,
MessengerActions,
@@ -40,9 +35,9 @@ export function getAccountsControllerMessenger(
],
events: [
'KeyringController:stateChange',
- SnapKeyringAccountAssetListUpdatedEvent,
- SnapKeyringAccountBalancesUpdatedEvent,
- SnapKeyringAccountTransactionsUpdatedEvent,
+ 'SnapAccountService:accountAssetListUpdated',
+ 'SnapAccountService:accountBalancesUpdated',
+ 'SnapAccountService:accountTransactionsUpdated',
'MultichainNetworkController:networkDidChange',
],
messenger,
diff --git a/app/core/Engine/messengers/index.ts b/app/core/Engine/messengers/index.ts
index 0275760bb56..74f8882a132 100644
--- a/app/core/Engine/messengers/index.ts
+++ b/app/core/Engine/messengers/index.ts
@@ -34,6 +34,7 @@ import { getMultichainAssetsRatesControllerMessenger } from './multichain-assets
import { getMultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger/multichain-assets-controller-messenger';
import { getMultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger/multichain-balances-controller-messenger';
import { getMultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger/multichain-transactions-controller-messenger';
+import { getSnapAccountServiceMessenger } from './snap-account-service-messenger/snap-account-service-messenger';
///: END:ONLY_INCLUDE_IF
import {
getTransactionControllerInitMessenger,
@@ -336,6 +337,10 @@ export const MESSENGER_FACTORIES = {
getMessenger: getMultichainTransactionsControllerMessenger,
getInitMessenger: noop,
},
+ SnapAccountService: {
+ getMessenger: getSnapAccountServiceMessenger,
+ getInitMessenger: noop,
+ },
///: END:ONLY_INCLUDE_IF
PermissionController: {
getMessenger: getPermissionControllerMessenger,
diff --git a/app/core/Engine/messengers/multichain-account-service-messenger/multichain-account-service-messenger.ts b/app/core/Engine/messengers/multichain-account-service-messenger/multichain-account-service-messenger.ts
index 0d74361a3c7..87ef7f39c4f 100644
--- a/app/core/Engine/messengers/multichain-account-service-messenger/multichain-account-service-messenger.ts
+++ b/app/core/Engine/messengers/multichain-account-service-messenger/multichain-account-service-messenger.ts
@@ -34,22 +34,21 @@ export function getMultichainAccountServiceMessenger(
'AccountsController:getAccountByAddress',
'AccountsController:getAccount',
'AccountsController:getAccounts',
- 'SnapController:handleRequest',
'KeyringController:getState',
'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2',
'KeyringController:addNewKeyring',
'KeyringController:getKeyringsByType',
'KeyringController:createNewVaultAndKeychain',
'KeyringController:createNewVaultAndRestore',
'NetworkController:getNetworkClientById',
'NetworkController:findNetworkClientIdByChainId',
- 'SnapController:getState',
+ 'SnapController:handleRequest',
+ 'SnapAccountService:ensureReady',
],
events: [
- 'KeyringController:stateChange',
'AccountsController:accountAdded',
'AccountsController:accountRemoved',
- 'SnapController:stateChange',
],
messenger,
});
diff --git a/app/core/Engine/messengers/multichain-routing-service-messenger.ts b/app/core/Engine/messengers/multichain-routing-service-messenger.ts
index 5c731a57c6c..29f1ea410e0 100644
--- a/app/core/Engine/messengers/multichain-routing-service-messenger.ts
+++ b/app/core/Engine/messengers/multichain-routing-service-messenger.ts
@@ -1,10 +1,7 @@
import { Messenger } from '@metamask/messenger';
import { MultichainRoutingServiceMessenger } from '@metamask/snaps-controllers';
-import {
- KeyringControllerAddNewKeyringAction,
- KeyringControllerGetKeyringsByTypeAction,
-} from '@metamask/keyring-controller';
import { RootMessenger } from '../types';
+import { SnapAccountServiceGetLegacySnapKeyringAction } from '@metamask/snap-account-service';
/**
* Get the multichain routing service messenger for the multichain routing
@@ -36,8 +33,7 @@ export function getMultichainRoutingServiceMessenger(
}
type AllowedInitializationActions =
- | KeyringControllerAddNewKeyringAction
- | KeyringControllerGetKeyringsByTypeAction;
+ SnapAccountServiceGetLegacySnapKeyringAction;
export type MultichainRoutingServiceInitMessenger = ReturnType<
typeof getMultichainRoutingServiceInitMessenger
@@ -65,10 +61,7 @@ export function getMultichainRoutingServiceInitMessenger(
});
rootMessenger.delegate({
- actions: [
- 'KeyringController:addNewKeyring',
- 'KeyringController:getKeyringsByType',
- ],
+ actions: ['SnapAccountService:getLegacySnapKeyring'],
events: [],
messenger,
});
diff --git a/app/core/Engine/messengers/permission-controller-messenger.ts b/app/core/Engine/messengers/permission-controller-messenger.ts
index 959ac70972e..92510b16b6a 100644
--- a/app/core/Engine/messengers/permission-controller-messenger.ts
+++ b/app/core/Engine/messengers/permission-controller-messenger.ts
@@ -18,6 +18,7 @@ import {
import { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller';
import { RootMessenger } from '../types.ts';
import { PermissionControllerMessenger } from '@metamask/permission-controller';
+import { SnapAccountServiceHandleKeyringSnapMessageAction } from '@metamask/snap-account-service';
/**
* Get the messenger for the permission controller. This is scoped to the
@@ -61,7 +62,8 @@ type AllowedInitializationActions =
| MultichainRoutingServiceIsSupportedScopeAction
| MultichainRoutingServiceGetSupportedAccountsAction
| NetworkControllerFindNetworkClientIdByChainIdAction
- | SnapPermissionSpecificationsActions;
+ | SnapPermissionSpecificationsActions
+ | SnapAccountServiceHandleKeyringSnapMessageAction;
type AllowedInitializationEvents = SnapPermissionSpecificationsEvents;
@@ -94,9 +96,9 @@ export function getPermissionControllerInitMessenger(
'ApprovalController:addRequest',
'AccountsController:listAccounts',
'CurrencyRateController:getState',
- 'KeyringController:getKeyringsByType',
'KeyringController:getState',
'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2Unsafe',
'MultichainRoutingService:isSupportedScope',
'MultichainRoutingService:getSupportedAccounts',
'NetworkController:findNetworkClientIdByChainId',
@@ -113,6 +115,7 @@ export function getPermissionControllerInitMessenger(
'SnapInterfaceController:getInterface',
'SnapInterfaceController:setInterfaceDisplayed',
'SnapInterfaceController:updateInterface',
+ 'SnapAccountService:handleKeyringSnapMessage',
],
events: ['KeyringController:unlock'],
messenger,
diff --git a/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.test.ts b/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.test.ts
new file mode 100644
index 00000000000..6d9198b4ba7
--- /dev/null
+++ b/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.test.ts
@@ -0,0 +1,136 @@
+import {
+ Messenger,
+ type MessengerActions,
+ type MessengerEvents,
+ MOCK_ANY_NAMESPACE,
+ type MockAnyNamespace,
+} from '@metamask/messenger';
+import type { SnapAccountServiceMessenger } from '@metamask/snap-account-service';
+import { getSnapAccountServiceMessenger } from './snap-account-service-messenger';
+
+type RootMessenger = Messenger<
+ MockAnyNamespace,
+ MessengerActions,
+ MessengerEvents
+>;
+
+function getRootMessenger(): RootMessenger {
+ return new Messenger({
+ namespace: MOCK_ANY_NAMESPACE,
+ });
+}
+
+describe('getSnapAccountServiceMessenger', () => {
+ it('returns a messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const snapAccountServiceMessenger =
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(snapAccountServiceMessenger).toBeInstanceOf(Messenger);
+ });
+
+ it('delegates the KeyringController actions to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actions: expect.arrayContaining([
+ 'KeyringController:withController',
+ 'KeyringController:getState',
+ 'KeyringController:withKeyringUnsafe',
+ ]),
+ }),
+ );
+ });
+
+ it('delegates the SnapController actions to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actions: expect.arrayContaining([
+ 'SnapController:getState',
+ 'SnapController:getSnap',
+ 'SnapController:getRunnableSnaps',
+ ]),
+ }),
+ );
+ });
+
+ it('delegates the AccountTreeController actions to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actions: expect.arrayContaining([
+ 'AccountTreeController:getAccountGroupObject',
+ 'AccountTreeController:getSelectedAccountGroup',
+ ]),
+ }),
+ );
+ });
+
+ it('delegates the KeyringController events to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ events: expect.arrayContaining([
+ 'KeyringController:stateChange',
+ 'KeyringController:unlock',
+ ]),
+ }),
+ );
+ });
+
+ it('delegates the SnapController events to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ events: expect.arrayContaining([
+ 'SnapController:stateChange',
+ 'SnapController:snapInstalled',
+ 'SnapController:snapEnabled',
+ 'SnapController:snapDisabled',
+ 'SnapController:snapBlocked',
+ 'SnapController:snapUnblocked',
+ 'SnapController:snapUninstalled',
+ ]),
+ }),
+ );
+ });
+
+ it('delegates the AccountTreeController events to the scoped messenger', () => {
+ const rootMessenger = getRootMessenger();
+ const delegateSpy = jest.spyOn(rootMessenger, 'delegate');
+
+ getSnapAccountServiceMessenger(rootMessenger);
+
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ events: expect.arrayContaining([
+ 'AccountTreeController:selectedAccountGroupChange',
+ 'AccountTreeController:accountGroupCreated',
+ 'AccountTreeController:accountGroupUpdated',
+ 'AccountTreeController:accountGroupRemoved',
+ ]),
+ }),
+ );
+ });
+});
diff --git a/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.ts b/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.ts
new file mode 100644
index 00000000000..592101f8951
--- /dev/null
+++ b/app/core/Engine/messengers/snap-account-service-messenger/snap-account-service-messenger.ts
@@ -0,0 +1,62 @@
+import {
+ Messenger,
+ MessengerActions,
+ MessengerEvents,
+} from '@metamask/messenger';
+import type { SnapAccountServiceMessenger as ServiceMessenger } from '@metamask/snap-account-service';
+import type { RootMessenger } from '../../types';
+
+type Actions = MessengerActions;
+type Events = MessengerEvents;
+
+export type SnapAccountServiceMessenger = ServiceMessenger;
+
+/**
+ * Get a restricted messenger for the snap account service. This is scoped to
+ * the actions and events that this service is allowed to handle.
+ *
+ * @param rootMessenger - The root messenger.
+ * @returns The SnapAccountServiceMessenger.
+ */
+export function getSnapAccountServiceMessenger(
+ rootMessenger: RootMessenger,
+): SnapAccountServiceMessenger {
+ const messenger = new Messenger<
+ 'SnapAccountService',
+ Actions,
+ Events,
+ RootMessenger
+ >({
+ namespace: 'SnapAccountService',
+ parent: rootMessenger,
+ });
+ rootMessenger.delegate({
+ messenger,
+ actions: [
+ 'KeyringController:withController',
+ 'KeyringController:getState',
+ 'KeyringController:withKeyringUnsafe',
+ 'SnapController:getState',
+ 'SnapController:getSnap',
+ 'SnapController:getRunnableSnaps',
+ 'AccountTreeController:getAccountGroupObject',
+ 'AccountTreeController:getSelectedAccountGroup',
+ ],
+ events: [
+ 'KeyringController:stateChange',
+ 'KeyringController:unlock',
+ 'SnapController:stateChange',
+ 'SnapController:snapInstalled',
+ 'SnapController:snapEnabled',
+ 'SnapController:snapDisabled',
+ 'SnapController:snapBlocked',
+ 'SnapController:snapUnblocked',
+ 'SnapController:snapUninstalled',
+ 'AccountTreeController:selectedAccountGroupChange',
+ 'AccountTreeController:accountGroupCreated',
+ 'AccountTreeController:accountGroupUpdated',
+ 'AccountTreeController:accountGroupRemoved',
+ ],
+ });
+ return messenger;
+}
diff --git a/app/core/Engine/messengers/snaps/snap-controller-messenger.ts b/app/core/Engine/messengers/snaps/snap-controller-messenger.ts
index 97941699b86..2fb267541d3 100644
--- a/app/core/Engine/messengers/snaps/snap-controller-messenger.ts
+++ b/app/core/Engine/messengers/snaps/snap-controller-messenger.ts
@@ -2,7 +2,7 @@ import { Messenger } from '@metamask/messenger';
import {
KeyringControllerLockEvent,
KeyringControllerUnlockEvent,
- KeyringControllerWithKeyringAction,
+ KeyringControllerWithKeyringV2UnsafeAction,
} from '@metamask/keyring-controller';
import { PreferencesControllerGetStateAction } from '@metamask/preferences-controller';
import { RootMessenger } from '../../types';
@@ -70,7 +70,7 @@ export function getSnapControllerMessenger(rootMessenger: RootMessenger) {
}
type InitActions =
- | KeyringControllerWithKeyringAction
+ | KeyringControllerWithKeyringV2UnsafeAction
| PreferencesControllerGetStateAction
| SnapControllerSetClientActiveAction
| AnalyticsControllerActions;
@@ -100,7 +100,7 @@ export function getSnapControllerInitMessenger(rootMessenger: RootMessenger) {
});
rootMessenger.delegate({
actions: [
- 'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2Unsafe',
'PreferencesController:getState',
'SnapController:setClientActive',
'AnalyticsController:trackEvent',
diff --git a/app/core/Engine/types.ts b/app/core/Engine/types.ts
index 91509b81dd7..93ea56f0814 100644
--- a/app/core/Engine/types.ts
+++ b/app/core/Engine/types.ts
@@ -377,6 +377,11 @@ import {
MultichainAccountServiceActions,
MultichainAccountServiceEvents,
} from '@metamask/multichain-account-service';
+import type {
+ SnapAccountService,
+ SnapAccountServiceActions,
+ SnapAccountServiceEvents,
+} from '@metamask/snap-account-service';
import {
GatorPermissionsController,
GatorPermissionsControllerActions,
@@ -517,7 +522,7 @@ type SnapsGlobalEvents =
| PhishingControllerEvents;
///: END:ONLY_INCLUDE_IF
-type GlobalActions =
+export type GlobalActions =
///: BEGIN:ONLY_INCLUDE_IF(sample-feature)
| SamplePetnamesControllerActions
///: END:ONLY_INCLUDE_IF
@@ -557,6 +562,7 @@ type GlobalActions =
| MultichainAssetsRatesControllerActions
| MultichainTransactionsControllerActions
| MultichainAccountServiceActions
+ | SnapAccountServiceActions
///: END:ONLY_INCLUDE_IF
| AccountsControllerActions
| AccountTreeControllerActions
@@ -607,7 +613,7 @@ type GlobalActions =
| ChompApiServiceActions
| MoneyAccountUpgradeControllerActions;
-type GlobalEvents =
+export type GlobalEvents =
///: BEGIN:ONLY_INCLUDE_IF(sample-feature)
| SamplePetnamesControllerEvents
///: END:ONLY_INCLUDE_IF
@@ -643,6 +649,7 @@ type GlobalEvents =
| MultichainAssetsRatesControllerEvents
| MultichainTransactionsControllerEvents
| MultichainAccountServiceEvents
+ | SnapAccountServiceEvents
///: END:ONLY_INCLUDE_IF
| SignatureControllerEvents
| LoggingControllerEvents
@@ -792,6 +799,7 @@ export type MessengerClients = {
MultichainRoutingService: MultichainRoutingService;
MultichainTransactionsController: MultichainTransactionsController;
MultichainAccountService: MultichainAccountService;
+ SnapAccountService: SnapAccountService;
///: END:ONLY_INCLUDE_IF
TokenSearchDiscoveryDataController: TokenSearchDiscoveryDataController;
MultichainNetworkController: MultichainNetworkController;
@@ -967,6 +975,7 @@ export type MessengerClientsToInitialize =
| 'MultichainRoutingService'
| 'MultichainTransactionsController'
| 'MultichainAccountService'
+ | 'SnapAccountService'
///: END:ONLY_INCLUDE_IF
| 'EarnController'
| 'MoneyAccountController'
diff --git a/app/core/Engine/utils/ensureOnboardingComplete.test.ts b/app/core/Engine/utils/ensureOnboardingComplete.test.ts
new file mode 100644
index 00000000000..6934a75ddc2
--- /dev/null
+++ b/app/core/Engine/utils/ensureOnboardingComplete.test.ts
@@ -0,0 +1,111 @@
+import { RootState } from '../../../reducers';
+import { store, runSaga } from '../../../store';
+import { ensureOnboardingComplete } from './ensureOnboardingComplete';
+import { Task } from 'redux-saga';
+
+jest.mock('../../../store', () => ({
+ store: { getState: jest.fn() },
+ runSaga: jest.fn(),
+}));
+
+const mockRunSaga = jest.mocked(runSaga);
+const mockGetState = jest.mocked(store.getState);
+
+function makeSagaTask(promise: Promise): Task {
+ return { toPromise: () => promise } as Task;
+}
+
+function setOnboardingComplete(complete: boolean) {
+ mockGetState.mockReturnValue({
+ onboarding: { completedOnboarding: complete },
+ } as unknown as RootState);
+}
+
+describe('ensureOnboardingComplete', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when onboarding is already complete', () => {
+ it('resolves immediately without starting a saga', async () => {
+ setOnboardingComplete(true);
+
+ await ensureOnboardingComplete();
+
+ expect(mockRunSaga).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when onboarding is not yet complete', () => {
+ it('starts a saga and waits for it to complete', async () => {
+ setOnboardingComplete(false);
+ mockRunSaga.mockReturnValue(makeSagaTask(Promise.resolve()));
+
+ await ensureOnboardingComplete();
+
+ expect(mockRunSaga).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not resolve until the saga completes', async () => {
+ setOnboardingComplete(false);
+
+ let resolveSaga!: () => void;
+ const sagaPromise = new Promise((resolve) => {
+ resolveSaga = resolve;
+ });
+ mockRunSaga.mockReturnValue(makeSagaTask(sagaPromise));
+
+ let resolved = false;
+ const pending = ensureOnboardingComplete().then(() => {
+ resolved = true;
+ });
+
+ expect(resolved).toBe(false);
+
+ resolveSaga();
+ await pending;
+
+ expect(resolved).toBe(true);
+ });
+
+ it('shares a single saga task across concurrent calls', async () => {
+ setOnboardingComplete(false);
+
+ let resolveSaga!: () => void;
+ const sagaPromise = new Promise((resolve) => {
+ resolveSaga = resolve;
+ });
+ mockRunSaga.mockReturnValue(makeSagaTask(sagaPromise));
+
+ const p1 = ensureOnboardingComplete();
+ const p2 = ensureOnboardingComplete();
+
+ expect(mockRunSaga).toHaveBeenCalledTimes(1);
+
+ resolveSaga();
+ await Promise.all([p1, p2]);
+ });
+
+ it('resets the shared promise after resolution so the next call starts a new saga', async () => {
+ setOnboardingComplete(false);
+ mockRunSaga.mockReturnValue(makeSagaTask(Promise.resolve()));
+
+ await ensureOnboardingComplete();
+ await ensureOnboardingComplete();
+
+ expect(mockRunSaga).toHaveBeenCalledTimes(2);
+ });
+
+ it('resets the shared promise after saga rejection so the next call starts a new saga', async () => {
+ setOnboardingComplete(false);
+ mockRunSaga.mockReturnValue(
+ makeSagaTask(Promise.reject(new Error('saga failed'))),
+ );
+
+ await expect(ensureOnboardingComplete()).rejects.toThrow('saga failed');
+ await expect(ensureOnboardingComplete()).rejects.toThrow('saga failed');
+
+ expect(mockRunSaga).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/app/core/Engine/utils/ensureOnboardingComplete.ts b/app/core/Engine/utils/ensureOnboardingComplete.ts
new file mode 100644
index 00000000000..d413526b6e1
--- /dev/null
+++ b/app/core/Engine/utils/ensureOnboardingComplete.ts
@@ -0,0 +1,46 @@
+import { take } from 'redux-saga/effects';
+import type { SagaIterator } from 'redux-saga';
+import { runSaga, store } from '../../../store';
+import { selectCompletedOnboarding } from '../../../selectors/onboarding';
+import {
+ SET_COMPLETED_ONBOARDING,
+ type SetCompletedOnboardingAction,
+} from '../../../actions/onboarding';
+
+let onboardingPromise: Promise | null = null;
+
+function* waitForOnboardingCompleteSaga(): SagaIterator {
+ while (true) {
+ const result = (yield take([
+ SET_COMPLETED_ONBOARDING,
+ ])) as SetCompletedOnboardingAction;
+
+ if (result.completedOnboarding) {
+ return;
+ }
+ }
+}
+
+/**
+ * Resolves when onboarding is complete. Resolves immediately when onboarding
+ * is already done; otherwise waits for the `SET_COMPLETED_ONBOARDING` action
+ * with `completedOnboarding: true`.
+ *
+ * The pending promise is shared across concurrent callers so a single saga
+ * task drives the wait.
+ */
+export async function ensureOnboardingComplete(): Promise {
+ if (selectCompletedOnboarding(store.getState())) {
+ return;
+ }
+
+ if (!onboardingPromise) {
+ onboardingPromise = runSaga(waitForOnboardingCompleteSaga).toPromise();
+ }
+
+ try {
+ await onboardingPromise;
+ } finally {
+ onboardingPromise = null;
+ }
+}
diff --git a/app/core/NotificationManager.js b/app/core/NotificationManager.js
index 2a45aaa4015..dd922aa602d 100644
--- a/app/core/NotificationManager.js
+++ b/app/core/NotificationManager.js
@@ -21,6 +21,7 @@ import {
import { endTrace, trace, TraceName } from '../util/trace';
import { hasTransactionType } from '../components/Views/confirmations/utils/transaction';
import TransactionTypes from './TransactionTypes';
+import { getNotificationSkipPredicates } from './notificationSkipPredicates';
export const SKIP_NOTIFICATION_TRANSACTION_TYPES = [
TransactionType.moneyAccountDeposit,
@@ -43,6 +44,14 @@ export const IN_PROGRESS_SKIP_STATUS = [
TransactionStatus.submitted,
];
+// Re-exported for convenience. The registry lives in its own dependency-free
+// module (`notificationSkipPredicates`) so feature code can register a predicate
+// without importing this heavy module and its transitive store/saga graph.
+export {
+ registerNotificationSkipPredicate,
+ clearNotificationSkipPredicates,
+} from './notificationSkipPredicates';
+
export const constructTitleAndMessage = (notification) => {
let title, message;
switch (notification.type) {
@@ -586,7 +595,24 @@ class NotificationManager {
tx.batchId === transactionMeta?.batchId,
);
- return isSameBatch;
+ if (isSameBatch) {
+ return true;
+ }
+
+ // Feature-registered predicates (e.g. QuickBuy surfaces its own toasts and
+ // opts its transactions out of the generic notification). A throwing
+ // predicate must never break notifications.
+ for (const predicate of getNotificationSkipPredicates()) {
+ try {
+ if (predicate(transactionMeta)) {
+ return true;
+ }
+ } catch (error) {
+ Logger.error(error, 'Notification skip predicate threw');
+ }
+ }
+
+ return false;
}
}
diff --git a/app/core/NotificationsManager.test.ts b/app/core/NotificationsManager.test.ts
index 73abedcd441..1bf777b1dc0 100644
--- a/app/core/NotificationsManager.test.ts
+++ b/app/core/NotificationsManager.test.ts
@@ -3,7 +3,9 @@ import { NotificationTransactionTypes } from '../util/notifications';
import NotificationManager, {
IN_PROGRESS_SKIP_STATUS,
SKIP_NOTIFICATION_TRANSACTION_TYPES,
+ clearNotificationSkipPredicates,
constructTitleAndMessage,
+ registerNotificationSkipPredicate,
} from './NotificationManager';
import { strings } from '../../locales/i18n';
import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller';
@@ -778,5 +780,83 @@ describe('NotificationManager', () => {
expect(showNotificationSpy).not.toHaveBeenCalled();
});
+
+ describe('notification skip predicates', () => {
+ afterEach(() => {
+ clearNotificationSkipPredicates();
+ });
+
+ it('does not show a notification when a registered predicate matches', () => {
+ registerNotificationSkipPredicate(() => true);
+
+ notificationManager.watchSubmittedTransaction({
+ id: '0x123',
+ txParams: { nonce: '0x1' },
+ silent: false,
+ });
+
+ expect(showNotificationSpy).not.toHaveBeenCalled();
+ });
+
+ it('shows a notification when no registered predicate matches', () => {
+ registerNotificationSkipPredicate(() => false);
+
+ notificationManager.watchSubmittedTransaction({
+ id: '0x123',
+ txParams: { nonce: '0x1' },
+ silent: false,
+ });
+
+ expect(showNotificationSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ type: 'pending' }),
+ );
+ });
+
+ it('ignores a throwing predicate and still shows the notification', () => {
+ registerNotificationSkipPredicate(() => {
+ throw new Error('predicate boom');
+ });
+
+ notificationManager.watchSubmittedTransaction({
+ id: '0x123',
+ txParams: { nonce: '0x1' },
+ silent: false,
+ });
+
+ expect(showNotificationSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ type: 'pending' }),
+ );
+ });
+
+ it('passes the transaction meta to the predicate', () => {
+ const predicate = jest.fn(() => false);
+ registerNotificationSkipPredicate(predicate);
+
+ notificationManager.watchSubmittedTransaction({
+ id: '0x123',
+ txParams: { nonce: '0x1' },
+ silent: false,
+ });
+
+ expect(predicate).toHaveBeenCalledWith(
+ expect.objectContaining({ id: '0x123' }),
+ );
+ });
+
+ it('stops showing notifications only while the predicate is registered', () => {
+ const unregister = registerNotificationSkipPredicate(() => true);
+ unregister();
+
+ notificationManager.watchSubmittedTransaction({
+ id: '0x123',
+ txParams: { nonce: '0x1' },
+ silent: false,
+ });
+
+ expect(showNotificationSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ type: 'pending' }),
+ );
+ });
+ });
});
});
diff --git a/app/core/RPCMethods/createEip5792Middleware.test.ts b/app/core/RPCMethods/createEip5792Middleware.test.ts
index a6fc60c7c5f..f6164832889 100644
--- a/app/core/RPCMethods/createEip5792Middleware.test.ts
+++ b/app/core/RPCMethods/createEip5792Middleware.test.ts
@@ -3,7 +3,6 @@ import { createEip5792Middleware } from './createEip5792Middleware';
describe('createEip5792Middleware', () => {
it('return instance of EIP-5792 Middleware', async () => {
const middleware = createEip5792Middleware({
- getAccounts: jest.fn(),
getCallsStatus: jest.fn(),
getCapabilities: jest.fn(),
processSendCalls: jest.fn(),
diff --git a/app/core/RPCMethods/createEip5792Middleware.ts b/app/core/RPCMethods/createEip5792Middleware.ts
index da2af5350b5..8c9ed09a8e3 100644
--- a/app/core/RPCMethods/createEip5792Middleware.ts
+++ b/app/core/RPCMethods/createEip5792Middleware.ts
@@ -11,31 +11,36 @@ import {
createScaffoldMiddleware,
} from '@metamask/json-rpc-engine';
import type { JsonRpcRequest } from '@metamask/utils';
+import { getPermittedAccounts } from '../Permissions';
+
+type JsonRpcRequestWithOrigin = JsonRpcRequest & { origin: string };
export function createEip5792Middleware({
- getAccounts,
getCallsStatus,
getCapabilities,
processSendCalls,
}: {
- getAccounts: (req: JsonRpcRequest) => Promise;
getCallsStatus: GetCallsStatusHook;
getCapabilities: GetCapabilitiesHook;
processSendCalls: ProcessSendCallsHook;
}) {
return createScaffoldMiddleware({
- wallet_getCapabilities: createAsyncMiddleware(async (req, res) =>
- walletGetCapabilities(req, res, {
- getAccounts,
+ wallet_getCapabilities: createAsyncMiddleware(async (req, res) => {
+ const request = req as unknown as JsonRpcRequestWithOrigin;
+ return walletGetCapabilities(request, res, {
+ getPermittedAccountsForOrigin: async () =>
+ getPermittedAccounts(request.origin),
getCapabilities,
- }),
- ),
- wallet_sendCalls: createAsyncMiddleware(async (req, res) =>
- walletSendCalls(req, res, {
- getAccounts,
+ });
+ }),
+ wallet_sendCalls: createAsyncMiddleware(async (req, res) => {
+ const request = req as unknown as JsonRpcRequestWithOrigin;
+ return walletSendCalls(request, res, {
+ getPermittedAccountsForOrigin: async () =>
+ getPermittedAccounts(request.origin),
processSendCalls,
- }),
- ),
+ });
+ }),
wallet_getCallsStatus: createAsyncMiddleware(async (req, res) =>
walletGetCallsStatus(req, res, {
getCallsStatus,
diff --git a/app/core/SDKConnectV2/adapters/host-application-adapter.test.ts b/app/core/SDKConnectV2/adapters/host-application-adapter.test.ts
index 22e38e07abd..260b2604d98 100644
--- a/app/core/SDKConnectV2/adapters/host-application-adapter.test.ts
+++ b/app/core/SDKConnectV2/adapters/host-application-adapter.test.ts
@@ -87,6 +87,20 @@ describe('HostApplicationAdapter', () => {
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
});
+
+ it('uses a custom autodismiss timeout when provided', () => {
+ adapter.showConnectionLoading(
+ createMockConnectionInfo('session-123', 'Test DApp'),
+ { autodismissMs: 15000 },
+ );
+
+ expect(showSimpleNotification).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'session-123',
+ autodismiss: 15000,
+ }),
+ );
+ });
});
describe('hideConnectionLoading', () => {
diff --git a/app/core/SDKConnectV2/adapters/host-application-adapter.ts b/app/core/SDKConnectV2/adapters/host-application-adapter.ts
index f87b56a50e3..008612a0bf6 100644
--- a/app/core/SDKConnectV2/adapters/host-application-adapter.ts
+++ b/app/core/SDKConnectV2/adapters/host-application-adapter.ts
@@ -1,5 +1,8 @@
import { Connection } from '../services/connection';
-import { IHostApplicationAdapter } from '../types/host-application-adapter';
+import {
+ IHostApplicationAdapter,
+ ShowConnectionLoadingOptions,
+} from '../types/host-application-adapter';
import { SDKSessions } from '../../../core/SDKConnect/SDKConnect';
import { store } from '../../../store';
import { setSdkV2Connections } from '../../../actions/sdk';
@@ -14,12 +17,23 @@ import Engine from '../../Engine';
import { Caip25EndowmentPermissionName } from '@metamask/chain-agnostic-permission';
import logger from '../services/logger';
+const DEFAULT_CONNECTION_LOADING_AUTODISMISS_MS = 10_000;
+
+/** Longer timeout for multi-step agentic CLI connect (MWP → OTP → dashboard). */
+export const AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS = 15_000;
+
export class HostApplicationAdapter implements IHostApplicationAdapter {
- showConnectionLoading(conninfo: ConnectionInfo): void {
+ showConnectionLoading(
+ conninfo: ConnectionInfo,
+ options?: ShowConnectionLoadingOptions,
+ ): void {
+ const autodismiss =
+ options?.autodismissMs ?? DEFAULT_CONNECTION_LOADING_AUTODISMISS_MS;
+
store.dispatch(
showSimpleNotification({
id: conninfo.id,
- autodismiss: 10000,
+ autodismiss,
title: strings('sdk_connect_v2.show_loading.title'),
description: strings('sdk_connect_v2.show_loading.description', {
dappName: conninfo.metadata.dapp.name,
diff --git a/app/core/SDKConnectV2/types/host-application-adapter.ts b/app/core/SDKConnectV2/types/host-application-adapter.ts
index 444d61daa83..ae7c22c49dd 100644
--- a/app/core/SDKConnectV2/types/host-application-adapter.ts
+++ b/app/core/SDKConnectV2/types/host-application-adapter.ts
@@ -1,6 +1,10 @@
import { Connection } from '../services/connection';
import { ConnectionInfo } from './connection-info';
+export interface ShowConnectionLoadingOptions {
+ autodismissMs?: number;
+}
+
/**
* Defines the contract for the host MetaMask Mobile application.
* This adapter is the sole boundary between the isolated SDKConnectV2 logic
@@ -13,7 +17,10 @@ export interface IHostApplicationAdapter {
* Displays a global, non-interactive loading modal. Used to indicate
* background activity, such as establishing a connection.
*/
- showConnectionLoading(conninfo: ConnectionInfo): void;
+ showConnectionLoading(
+ conninfo: ConnectionInfo,
+ options?: ShowConnectionLoadingOptions,
+ ): void;
/**
* Hides the global loading modal.
diff --git a/app/core/Snaps/permissions/specifications.test.ts b/app/core/Snaps/permissions/specifications.test.ts
new file mode 100644
index 00000000000..552b5c3564a
--- /dev/null
+++ b/app/core/Snaps/permissions/specifications.test.ts
@@ -0,0 +1,128 @@
+import {
+ Messenger,
+ MOCK_ANY_NAMESPACE,
+ type MockAnyNamespace,
+} from '@metamask/messenger';
+import { buildSnapRestrictedMethodSpecifications } from '@metamask/snaps-rpc-methods';
+import type { SnapMessage } from '@metamask/eth-snap-keyring';
+import type { SnapId } from '@metamask/snaps-sdk';
+import {
+ SnapPermissionSpecificationsActions,
+ SnapPermissionSpecificationsEvents,
+ getSnapPermissionSpecifications,
+} from './specifications';
+
+jest.mock('@metamask/snaps-rpc-methods', () => ({
+ buildSnapEndowmentSpecifications: jest.fn(() => ({})),
+ buildSnapRestrictedMethodSpecifications: jest.fn(() => ({})),
+}));
+
+const MOCK_SNAP_ID = 'npm:@metamask/test-snap' as SnapId;
+const MOCK_MESSAGE: SnapMessage = { method: 'keyring_createAccount' };
+
+function getMessenger() {
+ return new Messenger<
+ MockAnyNamespace,
+ SnapPermissionSpecificationsActions,
+ SnapPermissionSpecificationsEvents
+ >({ namespace: MOCK_ANY_NAMESPACE });
+}
+
+function getCapturedOptions() {
+ const { calls } = jest.mocked(buildSnapRestrictedMethodSpecifications).mock;
+ expect(calls.length).toBeGreaterThan(0);
+ // options are the second argument
+ return calls[calls.length - 1][1] as Record;
+}
+
+describe('getSnapPermissionSpecifications', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('getSnapKeyring', () => {
+ it('passes a getSnapKeyring option to buildSnapRestrictedMethodSpecifications', () => {
+ const messenger = getMessenger();
+ messenger.registerActionHandler(
+ 'SnapAccountService:handleKeyringSnapMessage',
+ jest.fn(),
+ );
+
+ getSnapPermissionSpecifications(messenger);
+
+ const options = getCapturedOptions();
+ expect(typeof options.getSnapKeyring).toBe('function');
+ });
+
+ it('returns a keyring object with a handleKeyringSnapMessage method', async () => {
+ const messenger = getMessenger();
+ messenger.registerActionHandler(
+ 'SnapAccountService:handleKeyringSnapMessage',
+ jest.fn().mockResolvedValue(null),
+ );
+
+ getSnapPermissionSpecifications(messenger);
+
+ const { getSnapKeyring } = getCapturedOptions() as {
+ getSnapKeyring: () => Promise;
+ };
+ const keyring = await getSnapKeyring();
+
+ expect(
+ typeof (keyring as Record).handleKeyringSnapMessage,
+ ).toBe('function');
+ });
+
+ it('delegates handleKeyringSnapMessage to SnapAccountService:handleKeyringSnapMessage', async () => {
+ const messenger = getMessenger();
+ const handleMock = jest.fn().mockResolvedValue({ address: '0xabc' });
+ messenger.registerActionHandler(
+ 'SnapAccountService:handleKeyringSnapMessage',
+ handleMock,
+ );
+
+ getSnapPermissionSpecifications(messenger);
+
+ const { getSnapKeyring } = getCapturedOptions() as {
+ getSnapKeyring: () => Promise<{
+ handleKeyringSnapMessage(
+ snapId: string,
+ message: SnapMessage,
+ ): Promise;
+ }>;
+ };
+ const keyring = await getSnapKeyring();
+ await keyring.handleKeyringSnapMessage(MOCK_SNAP_ID, MOCK_MESSAGE);
+
+ expect(handleMock).toHaveBeenCalledTimes(1);
+ expect(handleMock).toHaveBeenCalledWith(MOCK_SNAP_ID, MOCK_MESSAGE);
+ });
+
+ it('returns the result from SnapAccountService:handleKeyringSnapMessage', async () => {
+ const messenger = getMessenger();
+ const expectedResult = { address: '0xabc' };
+ messenger.registerActionHandler(
+ 'SnapAccountService:handleKeyringSnapMessage',
+ jest.fn().mockResolvedValue(expectedResult),
+ );
+
+ getSnapPermissionSpecifications(messenger);
+
+ const { getSnapKeyring } = getCapturedOptions() as {
+ getSnapKeyring: () => Promise<{
+ handleKeyringSnapMessage(
+ snapId: string,
+ message: SnapMessage,
+ ): Promise;
+ }>;
+ };
+ const keyring = await getSnapKeyring();
+ const result = await keyring.handleKeyringSnapMessage(
+ MOCK_SNAP_ID,
+ MOCK_MESSAGE,
+ );
+
+ expect(result).toBe(expectedResult);
+ });
+ });
+});
diff --git a/app/core/Snaps/permissions/specifications.ts b/app/core/Snaps/permissions/specifications.ts
index df9caf60dd9..336189aa8fb 100644
--- a/app/core/Snaps/permissions/specifications.ts
+++ b/app/core/Snaps/permissions/specifications.ts
@@ -18,11 +18,10 @@ import {
} from '@metamask/snaps-controllers';
import { CurrencyRateController } from '@metamask/assets-controllers';
import {
- KeyringControllerAddNewKeyringAction,
- KeyringControllerGetKeyringsByTypeAction,
KeyringControllerGetStateAction,
KeyringControllerUnlockEvent,
KeyringControllerWithKeyringAction,
+ KeyringControllerWithKeyringV2UnsafeAction,
KeyringTypes,
} from '@metamask/keyring-controller';
import { MaybeUpdateState, TestOrigin } from '@metamask/phishing-controller';
@@ -33,6 +32,9 @@ import { hmacSha512 } from '@metamask/native-utils';
import { pbkdf2 } from '../../Encryptor';
import I18n from '../../../../locales/i18n';
import { ExcludedSnapEndowments, ExcludedSnapPermissions } from './permissions';
+import { SnapMessage } from '@metamask/eth-snap-keyring';
+import { SnapId } from '@metamask/snaps-sdk';
+import { SnapAccountServiceHandleKeyringSnapMessageAction } from '@metamask/snap-account-service';
export type SnapPermissionSpecificationsActions =
| ApprovalControllerAddRequestAction
@@ -46,9 +48,8 @@ export type SnapPermissionSpecificationsActions =
| SnapControllerGetSnapAction
| SnapControllerGetSnapStateAction
| SnapControllerHandleRequestAction
- | KeyringControllerGetKeyringsByTypeAction
| KeyringControllerWithKeyringAction
- | KeyringControllerAddNewKeyringAction
+ | KeyringControllerWithKeyringV2UnsafeAction
| MaybeUpdateState
| PreferencesControllerGetStateAction
| TestOrigin
@@ -56,7 +57,8 @@ export type SnapPermissionSpecificationsActions =
| SnapInterfaceControllerUpdateInterfaceAction
| KeyringControllerGetStateAction
| HasPermission
- | SnapInterfaceControllerSetInterfaceDisplayedAction;
+ | SnapInterfaceControllerSetInterfaceDisplayedAction
+ | SnapAccountServiceHandleKeyringSnapMessageAction;
export type SnapPermissionSpecificationsEvents = KeyringControllerUnlockEvent;
@@ -122,27 +124,17 @@ export const getSnapPermissionSpecifications = (
},
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
- getSnapKeyring: async () => {
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- let [snapKeyring] = messenger.call(
- 'KeyringController:getKeyringsByType',
- KeyringTypes.snap,
- );
-
- if (!snapKeyring) {
- await messenger.call(
- 'KeyringController:addNewKeyring',
- KeyringTypes.snap,
+ getSnapKeyring: async () => ({
+ // We only need a subset of the Snap keyring's functionality, and this message handling is now
+ // owned by the Snap account service.
+ handleKeyringSnapMessage(snapId: string, message: SnapMessage) {
+ return messenger.call(
+ 'SnapAccountService:handleKeyringSnapMessage',
+ snapId as SnapId,
+ message,
);
- // TODO: Replace `getKeyringsByType` with `withKeyring`
- [snapKeyring] = messenger.call(
- 'KeyringController:getKeyringsByType',
- KeyringTypes.snap,
- );
- }
-
- return snapKeyring;
- },
+ },
+ }),
///: END:ONLY_INCLUDE_IF
},
messenger as RestrictedMethodMessenger,
diff --git a/app/core/Snaps/permissions/utils.test.ts b/app/core/Snaps/permissions/utils.test.ts
index 1fb644aba5a..9e46dabd8fc 100644
--- a/app/core/Snaps/permissions/utils.test.ts
+++ b/app/core/Snaps/permissions/utils.test.ts
@@ -1,16 +1,16 @@
import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger';
import { getMnemonicSeed } from './utils';
-import { KeyringControllerWithKeyringAction } from '@metamask/keyring-controller';
+import { KeyringControllerWithKeyringV2UnsafeAction } from '@metamask/keyring-controller';
import { HdKeyring } from '@metamask/eth-hd-keyring';
+import { HdKeyring as HdKeyringV2 } from '@metamask/eth-hd-keyring/v2';
import { hexToBytes } from '@metamask/utils';
-import { mnemonicPhraseToBytes } from '@metamask/key-tree';
import { mnemonicToSeed } from 'ethers/lib/utils';
-import { Keyring } from '@metamask/keyring-utils';
+import { Keyring } from '@metamask/keyring-api/v2';
import { LedgerKeyring } from '@metamask/eth-ledger-bridge-keyring';
+import { LedgerKeyring as LedgerKeyringV2 } from '@metamask/eth-ledger-bridge-keyring/v2';
const TEST_MNEMONIC =
'test test test test test test test test test test test ball';
-const TEST_MNEMONIC_BYTES = mnemonicPhraseToBytes(TEST_MNEMONIC);
const TEST_MNEMONIC_SEED = hexToBytes(mnemonicToSeed(TEST_MNEMONIC));
/**
@@ -32,17 +32,23 @@ async function getMessenger(deserialize = true) {
const ledgerKeyring = new LedgerKeyring({ bridge: {} });
const keyrings: Record = {
- main: hdKeyring,
- ledger: ledgerKeyring,
+ main: new HdKeyringV2({
+ legacyKeyring: hdKeyring,
+ entropySource: 'mock-hd-keyring-id',
+ }),
+ ledger: new LedgerKeyringV2({
+ legacyKeyring: ledgerKeyring,
+ entropySource: 'mock-ledger-keyring-id',
+ }),
};
const messenger = new Messenger<
string,
- KeyringControllerWithKeyringAction,
+ KeyringControllerWithKeyringV2UnsafeAction,
never
>({ namespace: MOCK_ANY_NAMESPACE });
messenger.registerActionHandler(
- 'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2Unsafe',
(selector, operation) => {
if ('type' in selector) {
const [id, keyring] = Object.entries(keyrings).filter(
diff --git a/app/core/Snaps/permissions/utils.ts b/app/core/Snaps/permissions/utils.ts
index 662d709898f..6408a103d0a 100644
--- a/app/core/Snaps/permissions/utils.ts
+++ b/app/core/Snaps/permissions/utils.ts
@@ -1,8 +1,6 @@
-import {
- KeyringControllerWithKeyringAction,
- KeyringTypes,
-} from '@metamask/keyring-controller';
-import type { HdKeyring } from '@metamask/eth-hd-keyring';
+import { KeyringControllerWithKeyringV2UnsafeAction } from '@metamask/keyring-controller';
+import type { HdKeyring } from '@metamask/eth-hd-keyring/v2';
+import { KeyringType } from '@metamask/keyring-api/v2';
import { Messenger } from '@metamask/messenger';
/**
@@ -14,14 +12,18 @@ import { Messenger } from '@metamask/messenger';
* @returns The mnemonic seed.
*/
export async function getMnemonicSeed(
- messenger: Messenger,
+ messenger: Messenger<
+ string,
+ KeyringControllerWithKeyringV2UnsafeAction,
+ never
+ >,
source?: string | undefined,
): Promise {
if (!source) {
const seed = (await messenger.call(
- 'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2Unsafe',
{
- type: KeyringTypes.hd,
+ type: KeyringType.Hd,
index: 0,
},
async ({ keyring }) => (keyring as HdKeyring).seed,
@@ -36,7 +38,7 @@ export async function getMnemonicSeed(
try {
const keyringData = await messenger.call(
- 'KeyringController:withKeyring',
+ 'KeyringController:withKeyringV2Unsafe',
{
id: source,
},
@@ -48,7 +50,7 @@ export async function getMnemonicSeed(
const { type, seed } = keyringData as { type: string; seed?: Uint8Array };
- if (type !== KeyringTypes.hd || !seed) {
+ if (type !== KeyringType.Hd || !seed) {
// The keyring isn't guaranteed to have a mnemonic (e.g.,
// hardware wallets, which can't be used as entropy sources),
// so we throw an error if it doesn't.
diff --git a/app/core/notificationSkipPredicates.ts b/app/core/notificationSkipPredicates.ts
new file mode 100644
index 00000000000..d88fc31f9e7
--- /dev/null
+++ b/app/core/notificationSkipPredicates.ts
@@ -0,0 +1,40 @@
+import type { TransactionMeta } from '@metamask/transaction-controller';
+
+/**
+ * A predicate that, given a transaction, returns `true` when the generic
+ * transaction notification should be suppressed for it.
+ */
+export type NotificationSkipPredicate = (
+ transactionMeta: TransactionMeta | undefined,
+) => boolean;
+
+/**
+ * Registry of predicates that let feature code opt a transaction out of the
+ * generic transaction notifications, without `NotificationManager` (core)
+ * depending on any feature module.
+ *
+ * This lives in its own dependency-free module so feature code can register a
+ * predicate without importing the heavy `NotificationManager` module (and its
+ * transitive store/saga graph).
+ */
+const notificationSkipPredicates = new Set();
+
+/**
+ * Register a predicate. Returns an unregister function.
+ */
+export function registerNotificationSkipPredicate(
+ predicate: NotificationSkipPredicate,
+): () => void {
+ notificationSkipPredicates.add(predicate);
+ return () => {
+ notificationSkipPredicates.delete(predicate);
+ };
+}
+
+export function clearNotificationSkipPredicates(): void {
+ notificationSkipPredicates.clear();
+}
+
+export function getNotificationSkipPredicates(): NotificationSkipPredicate[] {
+ return Array.from(notificationSkipPredicates);
+}
diff --git a/app/features/SampleFeature/e2e/pages/SampleFeatureView.ts b/app/features/SampleFeature/e2e/pages/SampleFeatureView.ts
index 14c8b9927ac..e3ed5817a8d 100644
--- a/app/features/SampleFeature/e2e/pages/SampleFeatureView.ts
+++ b/app/features/SampleFeature/e2e/pages/SampleFeatureView.ts
@@ -2,72 +2,65 @@
import Matchers from '../../../../../tests/framework/Matchers';
import Gestures from '../../../../../tests/framework/Gestures';
import Assertions from '../../../../../tests/framework/Assertions';
+import { type EncapsulatedElementType } from '../../../../../tests/framework/EncapsulatedElement';
import {
SampleFeatureSelectorsIDs,
SampleFeatureSelectorsText,
} from '../selectors/SampleFeature.selectors';
class SampleFeatureView {
- get container(): Promise {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
SampleFeatureSelectorsIDs.SAMPLE_FEATURE_CONTAINER,
- ) as Promise;
+ );
}
- get title(): Promise {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(
SampleFeatureSelectorsText.SAMPLE_FEATURE_TITLE,
);
}
- get description(): Promise {
+ get description(): EncapsulatedElementType {
return Matchers.getElementByText(
SampleFeatureSelectorsText.SAMPLE_FEATURE_DESCRIPTION,
);
}
- get counterTitle(): Promise {
+ get counterTitle(): EncapsulatedElementType {
return Matchers.getElementByID(
SampleFeatureSelectorsIDs.SAMPLE_COUNTER_PANE_TITLE,
- ) as Promise;
+ );
}
- get counterValue(): Promise {
+ get counterValue(): EncapsulatedElementType {
return Matchers.getElementByID(
SampleFeatureSelectorsIDs.SAMPLE_COUNTER_PANE_VALUE,
- ) as Promise;
+ );
}
- get incrementButton(): Promise {
+ get incrementButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SampleFeatureSelectorsIDs.SAMPLE_COUNTER_PANE_INCREMENT_BUTTON,
- ) as Promise;
+ );
}
- get networkImage(): Promise {
+ get networkImage(): EncapsulatedElementType {
// Assuming the network image has a testID
- return Matchers.getElementByID(
- 'network-avatar-image',
- ) as Promise;
+ return Matchers.getElementByID('network-avatar-image');
}
// Pet Name Elements
- get petNameAddressInput(): Promise {
- return Matchers.getElementByID(
- 'pet-name-address-input',
- ) as Promise;
+ get petNameAddressInput(): EncapsulatedElementType {
+ return Matchers.getElementByID('pet-name-address-input');
}
- get petNameNameInput(): Promise {
- return Matchers.getElementByID(
- 'pet-name-name-input',
- ) as Promise;
+ get petNameNameInput(): EncapsulatedElementType {
+ return Matchers.getElementByID('pet-name-name-input');
}
- get addPetNameButton(): Promise {
- return Matchers.getElementByID(
- 'add-pet-name-button',
- ) as Promise;
+ get addPetNameButton(): EncapsulatedElementType {
+ return Matchers.getElementByID('add-pet-name-button');
}
async tapIncrementButton(): Promise {
diff --git a/app/hooks/useMessenger.ts b/app/hooks/useMessenger.ts
new file mode 100644
index 00000000000..335719e1092
--- /dev/null
+++ b/app/hooks/useMessenger.ts
@@ -0,0 +1,43 @@
+import { useRouteMessenger } from '../contexts/route-messenger';
+import type { RouteMessenger } from '../messengers/route-messenger';
+
+/**
+ * Use this hook to access the messenger that corresponds to the current route.
+ * You can then use the messenger to call actions and subscribe to events that
+ * the route has permission to access.
+ *
+ * Supply a {@link RouteMessenger} type as the type parameter to narrow the
+ * returned messenger to only the actions and events you need.
+ *
+ * @returns The route messenger cast to the requested messenger type.
+ * @example
+ * ```typescript
+ * type NetworkListMessenger = RouteMessenger<
+ * 'NetworkController:addNetwork',
+ * never
+ * >;
+ *
+ * function NetworkList() {
+ * const messenger = useMessenger();
+ *
+ * const onAddNetworkClick = async () => {
+ * await messenger.call('NetworkController:addNetwork', ...);
+ * }
+ *
+ * return (
+ *
+ * ):
+ * }
+ * ```
+ */
+export function useMessenger<
+ RouteMessengerInstance extends RouteMessenger,
+>(): RouteMessengerInstance {
+ const routeMessenger = useRouteMessenger();
+
+ // The context only tells us we have *some* kind of route messenger, but not
+ // what capabilities it has. We trust the caller's type parameter here — the
+ // route configuration is responsible for ensuring the messenger actually
+ // supports the requested actions/events.
+ return routeMessenger as unknown as RouteMessengerInstance;
+}
diff --git a/app/messengers/configs/helpers.ts b/app/messengers/configs/helpers.ts
new file mode 100644
index 00000000000..aa33887b1b5
--- /dev/null
+++ b/app/messengers/configs/helpers.ts
@@ -0,0 +1,26 @@
+import type { ValidateElements } from '../helpers/route-messenger-helpers';
+import { GlobalActions, GlobalEvents } from '../../core/Engine';
+
+/**
+ * Helper function to define the excluded capabilities for a messenger. This is
+ * primarily a type-level helper to ensure that the excluded capabilities are
+ * valid and to get better type inference for the excluded capabilities.
+ *
+ * @param capabilities - The capabilities to exclude, which must be valid action
+ * and event types for the `RootMessenger`.
+ * @param capabilities.actions - The action types to exclude, which must be
+ * valid action types for the `RootMessenger`.
+ * @param capabilities.events - The event types to exclude, which must be valid
+ * event types for the `RootMessenger`.
+ * @returns The given capabilities, typed as the specific action and event types
+ * that were excluded.
+ */
+export function defineExcludedCapabilities<
+ const ActionTypes extends readonly string[],
+ const EventTypes extends readonly string[],
+>(capabilities: {
+ actions: ValidateElements;
+ events: ValidateElements;
+}): { actions: ActionTypes; events: EventTypes } {
+ return capabilities as { actions: ActionTypes; events: EventTypes };
+}
diff --git a/app/messengers/configs/index.ts b/app/messengers/configs/index.ts
new file mode 100644
index 00000000000..110c0b69b2a
--- /dev/null
+++ b/app/messengers/configs/index.ts
@@ -0,0 +1,22 @@
+/* eslint-disable import-x/no-namespace */
+
+import * as keyringController from './keyring-controller';
+import { GlobalActions, GlobalEvents } from '../../core/Engine';
+
+interface MessengerExclusions {
+ // This is named like this since it's intended to be defined as a top-level
+ // property of a messenger configuration.
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ EXCLUDED_CAPABILITIES: {
+ actions: readonly GlobalActions['type'][];
+ events: readonly GlobalEvents['type'][];
+ };
+}
+
+// If you need to exclude an action or event from being accessible in the UI,
+// add a file to this directory and then add the module to this list.
+// Note: A file does not need to exist for every controller or service, just
+// those that need to exclude certain actions and/or events.
+export const MESSENGERS_WITH_EXCLUSIONS = [
+ keyringController,
+] satisfies MessengerExclusions[];
diff --git a/app/messengers/configs/keyring-controller.ts b/app/messengers/configs/keyring-controller.ts
new file mode 100644
index 00000000000..9acd56f94a8
--- /dev/null
+++ b/app/messengers/configs/keyring-controller.ts
@@ -0,0 +1,17 @@
+import { defineExcludedCapabilities } from './helpers';
+
+// By default, all actions and events are allowed. If there an action or event
+// you think should NOT be accessible from the UI, update this.
+export const EXCLUDED_CAPABILITIES = defineExcludedCapabilities({
+ actions: [
+ 'KeyringController:addNewKeyring',
+ 'KeyringController:getKeyringsByType',
+ 'KeyringController:getKeyringForAccount',
+ 'KeyringController:withController',
+ 'KeyringController:withKeyring',
+ 'KeyringController:withKeyringUnsafe',
+ 'KeyringController:withKeyringV2',
+ 'KeyringController:withKeyringV2Unsafe',
+ ],
+ events: [],
+});
diff --git a/app/messengers/helpers/route-messenger-helpers.test.tsx b/app/messengers/helpers/route-messenger-helpers.test.tsx
new file mode 100644
index 00000000000..5af693798ba
--- /dev/null
+++ b/app/messengers/helpers/route-messenger-helpers.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { withMessenger } from './route-messenger-helpers';
+
+describe('withMessenger', () => {
+ const FooComponent = () => Foo
;
+
+ it('returns a Route component with the expected shape', () => {
+ const Route = withMessenger(FooComponent, {
+ capabilities: {
+ actions: ['SnapController:installSnaps'],
+ events: ['SnapController:snapInstalled'],
+ },
+ });
+
+ expect(
+ ,
+ ).toMatchInlineSnapshot(`
+
+ `);
+ });
+});
diff --git a/app/messengers/helpers/route-messenger-helpers.tsx b/app/messengers/helpers/route-messenger-helpers.tsx
new file mode 100644
index 00000000000..387a2ec1408
--- /dev/null
+++ b/app/messengers/helpers/route-messenger-helpers.tsx
@@ -0,0 +1,125 @@
+import React, { ComponentType, FunctionComponent } from 'react';
+import { UIMessengerActions, UIMessengerEvents } from '../ui-messenger';
+import { RouteWithMessenger } from '../route-with-messenger';
+import {
+ NavigationProp,
+ EventMapBase,
+ RouteProp,
+ ParamListBase,
+ NavigationState,
+} from '@react-navigation/native';
+
+/**
+ * Validate that each element of a tuple is a member of the given union,
+ * producing a type error for elements that aren't.
+ */
+export type ValidateElements<
+ Elements extends readonly string[],
+ AllowedElements extends string,
+> = {
+ [K in keyof Elements]: Elements[K] extends AllowedElements
+ ? Elements[K]
+ : AllowedElements;
+};
+
+/**
+ * Helper function to define the allowed capabilities for a route messenger.
+ * This is primarily a type-level helper to ensure that the allowed capabilities
+ * are valid UI messenger capabilities and to get better type inference for the
+ * allowed capabilities.
+ *
+ * @param capabilities - The capabilities to allow, which must be valid action
+ * and event types for the `UIMessenger`.
+ * @param capabilities.actions - The action types to allow, which must be
+ * valid action types for the `UIMessenger`.
+ * @param capabilities.events - The event types to allow, which must be valid
+ * event types for the `UIMessenger`.
+ * @returns The given capabilities, typed as the specific action and event types
+ * that were allowed.
+ */
+export function defineAllowedRouteCapabilities<
+ const ActionTypes extends string[],
+ const EventTypes extends string[],
+>(capabilities: {
+ actions: ValidateElements;
+ events: ValidateElements;
+}): { actions: ActionTypes; events: EventTypes } {
+ return capabilities as { actions: ActionTypes; events: EventTypes };
+}
+
+interface WithMessengerOptions {
+ capabilities: {
+ actions?: UIMessengerActions['type'][];
+ events?: UIMessengerEvents['type'][];
+ };
+}
+
+interface RouteProps<
+ ParamList extends ParamListBase,
+ RouteName extends keyof ParamList,
+ NavigatorID extends string | undefined = undefined,
+ State extends NavigationState = NavigationState,
+ ScreenOptions extends {} = {},
+ EventMap extends EventMapBase = {},
+> {
+ route: RouteProp;
+ navigation: NavigationProp<
+ ParamList,
+ RouteName,
+ NavigatorID,
+ State,
+ ScreenOptions,
+ EventMap
+ >;
+}
+
+/**
+ * Create a route object with a {@link RouteWithMessenger} element that provides
+ * a route messenger with the specified capabilities.
+ *
+ * @param Component - The component to render for this route. This will be
+ * wrapped in a {@link RouteWithMessenger} component that provides the route
+ * messenger.
+ * @param options - Options bag.
+ * @param options.capabilities - Capabilities to delegate from the UI messenger
+ * to the route messenger.
+ */
+export function withMessenger<
+ ParamList extends ParamListBase,
+ RouteName extends keyof ParamList,
+ NavigatorID extends string | undefined = undefined,
+ State extends NavigationState = NavigationState,
+ ScreenOptions extends {} = {},
+ EventMap extends EventMapBase = {},
+>(
+ Component: ComponentType<
+ RouteProps<
+ ParamList,
+ RouteName,
+ NavigatorID,
+ State,
+ ScreenOptions,
+ EventMap
+ >
+ >,
+ { capabilities }: WithMessengerOptions,
+): FunctionComponent<
+ RouteProps
+> {
+ return function RouteWithMessengerElement(
+ props: RouteProps<
+ ParamList,
+ RouteName,
+ NavigatorID,
+ State,
+ ScreenOptions,
+ EventMap
+ >,
+ ) {
+ return (
+
+
+
+ );
+ };
+}
diff --git a/app/messengers/route-messenger.test.ts b/app/messengers/route-messenger.test.ts
new file mode 100644
index 00000000000..d6ac2abf80d
--- /dev/null
+++ b/app/messengers/route-messenger.test.ts
@@ -0,0 +1,38 @@
+// eslint-disable-next-line import-x/no-namespace
+import * as messengerModule from '@metamask/messenger';
+import {
+ createRouteMessenger,
+ getRouteMessengerNamespace,
+} from './route-messenger';
+
+describe('getRouteMessengerNamespace', () => {
+ it.each([
+ ['SomePath', 'SomePathRoute'],
+ ['AnotherExample', 'AnotherExampleRoute'],
+ ['', 'Route'],
+ ])(
+ 'derives the correct namespace from the path "%s"',
+ (path, expectedNamespace) => {
+ expect(getRouteMessengerNamespace(path)).toBe(expectedNamespace);
+ },
+ );
+});
+
+describe('createRouteMessenger', () => {
+ it('creates a route messenger with the correct namespace', () => {
+ const OriginalMessenger = messengerModule.Messenger;
+ const MessengerSpy = jest
+ .spyOn(messengerModule, 'Messenger')
+ .mockImplementation(
+ (...args: ConstructorParameters) =>
+ new OriginalMessenger(...args),
+ );
+
+ createRouteMessenger({ path: 'SomePath' });
+
+ expect(MessengerSpy).toHaveBeenCalledWith({
+ namespace: 'SomePathRoute',
+ captureException: expect.any(Function),
+ });
+ });
+});
diff --git a/app/messengers/route-messenger.ts b/app/messengers/route-messenger.ts
new file mode 100644
index 00000000000..c96b8c4482f
--- /dev/null
+++ b/app/messengers/route-messenger.ts
@@ -0,0 +1,77 @@
+import { Messenger } from '@metamask/messenger';
+import type { UIMessengerActions, UIMessengerEvents } from './ui-messenger';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Helper type to derive a route messenger type from a capabilities object.
+ */
+export type RouteMessengerFromCapabilities<
+ CapabilitiesType extends {
+ actions?: UIMessengerActions['type'][];
+ events?: UIMessengerEvents['type'][];
+ },
+> = RouteMessenger<
+ CapabilitiesType extends { actions: (infer ActionTypes)[] }
+ ? ActionTypes
+ : never,
+ CapabilitiesType extends { events: (infer EventTypes)[] } ? EventTypes : never
+>;
+
+/**
+ * A messenger that represents a route.
+ *
+ * This type is intentionally generic (a bit unusual for messenger "instance"
+ * types) because each route gets its own messenger (the "route messenger" isn't
+ * a singleton as is the case for controllers and services).
+ */
+export type RouteMessenger<
+ ActionTypes extends UIMessengerActions['type'] = UIMessengerActions['type'],
+ EventTypes extends UIMessengerEvents['type'] = UIMessengerEvents['type'],
+> = Messenger<
+ `${string}Route`,
+ Extract,
+ Extract
+>;
+
+/**
+ * Derive a route messenger namespace from a route path. Assumes the path is
+ * already formatted as PascalCase (e.g. 'SomePath' rather than '/some/path').
+ *
+ * @example
+ * ```typescript
+ * getRouteMessengerNamespace('SomePath'); // 'SomePathRoute'
+ * getRouteMessengerNamespace('AnotherExample'); // 'AnotherExampleRoute'
+ * getRouteMessengerNamespace(''); // 'Route'
+ * ```
+ * @param path - The route path to derive the namespace from.
+ * @returns A namespace string derived from the route path.
+ */
+export function getRouteMessengerNamespace(
+ path: string = '',
+): `${string}Route` {
+ return `${path}Route`;
+}
+
+/**
+ * Create a messenger for a route.
+ *
+ * This is used when defining routes (that is, each route gets its own
+ * messenger). Delegation of actions and events is handled separately by the
+ * caller (typically via `RouteWithMessenger`).
+ *
+ * @param args - Arguments for this function.
+ * @param args.path - The path of the route. This is used for debugging purposes
+ * and to ensure that the route messenger's namespace is unique across routes.
+ * @returns A messenger for the route.
+ */
+export function createRouteMessenger<
+ ActionTypes extends UIMessengerActions['type'],
+ EventTypes extends UIMessengerEvents['type'],
+>({ path }: { path: string }): RouteMessenger {
+ return new Messenger<`${string}Route`, UIMessengerActions, UIMessengerEvents>(
+ {
+ namespace: getRouteMessengerNamespace(path),
+ captureException,
+ },
+ );
+}
diff --git a/app/messengers/route-with-messenger.test.tsx b/app/messengers/route-with-messenger.test.tsx
new file mode 100644
index 00000000000..51df80e4551
--- /dev/null
+++ b/app/messengers/route-with-messenger.test.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { act } from '@testing-library/react-native';
+import { View } from 'react-native';
+// eslint-disable-next-line import-x/no-namespace
+import * as routeMessengerModule from '../messengers/route-messenger';
+import { RouteWithMessenger } from './route-with-messenger';
+import renderWithProvider from '../util/test/renderWithProvider';
+import { createMockUIMessenger } from '../util/test/mock-ui-messenger';
+import type { UIMessengerActions, UIMessengerEvents } from './ui-messenger';
+
+const CAPABILITIES = {
+ actions: [
+ 'SnapController:installSnaps' as unknown as UIMessengerActions['type'],
+ ],
+ events: [
+ 'SnapController:snapInstalled' as unknown as UIMessengerEvents['type'],
+ ],
+};
+
+describe('RouteWithMessenger', () => {
+ it('renders children once delegation is complete', async () => {
+ const uiMessenger = createMockUIMessenger();
+ jest.spyOn(uiMessenger, 'delegate').mockResolvedValue(undefined);
+
+ const { findByTestId } = renderWithProvider(
+
+
+ ,
+ { uiMessenger },
+ );
+
+ expect(await findByTestId('child')).toBeOnTheScreen();
+ });
+
+ it('delegates capabilities to the route messenger on mount', async () => {
+ const uiMessenger = createMockUIMessenger();
+ const delegateSpy = jest
+ .spyOn(uiMessenger, 'delegate')
+ .mockResolvedValue(undefined);
+
+ const createRouteMessengerSpy = jest.spyOn(
+ routeMessengerModule,
+ 'createRouteMessenger',
+ );
+
+ const { findByTestId } = renderWithProvider(
+
+
+ ,
+ { uiMessenger },
+ );
+
+ // Wait for delegation to complete.
+ await findByTestId('content');
+
+ expect(createRouteMessengerSpy).toHaveBeenCalledWith({ path: 'SomePath' });
+ expect(delegateSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actions: CAPABILITIES.actions,
+ events: CAPABILITIES.events,
+ }),
+ );
+ });
+
+ it('revokes delegation when the component unmounts', async () => {
+ const uiMessenger = createMockUIMessenger();
+ jest.spyOn(uiMessenger, 'delegate').mockResolvedValue(undefined);
+
+ const revokeSpy = jest
+ .spyOn(uiMessenger, 'revoke')
+ .mockResolvedValue(undefined);
+
+ const { findByTestId, unmount } = renderWithProvider(
+
+
+ ,
+ { uiMessenger },
+ );
+
+ // Wait for delegation to complete before unmounting.
+ await findByTestId('content');
+
+ await act(async () => {
+ unmount();
+ });
+
+ expect(revokeSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actions: CAPABILITIES.actions,
+ events: CAPABILITIES.events,
+ }),
+ );
+ });
+});
diff --git a/app/messengers/route-with-messenger.tsx b/app/messengers/route-with-messenger.tsx
new file mode 100644
index 00000000000..bb2679aa706
--- /dev/null
+++ b/app/messengers/route-with-messenger.tsx
@@ -0,0 +1,84 @@
+import React, { ReactNode, useEffect, useRef, useState } from 'react';
+import { UIMessengerActions, UIMessengerEvents } from './ui-messenger';
+import { RouteMessengerContext } from '../contexts/route-messenger';
+import { useUIMessenger } from '../contexts/ui-messenger';
+import { createRouteMessenger, type RouteMessenger } from './route-messenger';
+import { captureException } from '@sentry/react-native';
+
+/**
+ * Utility component which creates a messenger representing a route and
+ * provides it to children via context.
+ *
+ * Do not use this component directly. Instead, use the `withMessenger` HOC to
+ * wrap route components, which will render this component internally.
+ *
+ * @param props - Component props.
+ * @param props.path - The path of the route. This is used for debugging
+ * purposes and to ensure that the route messenger's namespace is unique across
+ * routes.
+ * @param props.capabilities - Capabilities to delegate to the route messenger.
+ * This must be a stable object (i.e. it should be memoized if defined inline)
+ * to avoid unnecessary re-delegation on every render.
+ * @param props.capabilities.actions - Action types to delegate to the route
+ * messenger.
+ * @param props.capabilities.events - Event types to delegate to the route
+ * messenger.
+ * @param props.children - Child components.
+ */
+export const RouteWithMessenger = ({
+ path,
+ capabilities,
+ children,
+}: {
+ path: string;
+ capabilities: {
+ actions?: UIMessengerActions['type'][];
+ events?: UIMessengerEvents['type'][];
+ };
+ children: ReactNode;
+}) => {
+ const uiMessenger = useUIMessenger();
+ const routeMessengerRef = useRef(null);
+ const [isDelegated, setDelegated] = useState(false);
+
+ // `useMemo` doesn't work here because `capabilities` is an object, so we use
+ // a ref instead to ensure that we only create the route messenger once.
+ if (!routeMessengerRef.current) {
+ routeMessengerRef.current = createRouteMessenger({ path });
+ }
+
+ useEffect(() => {
+ const messenger = routeMessengerRef.current;
+ if (!messenger) {
+ return;
+ }
+
+ const { actions, events } = capabilities;
+ uiMessenger
+ .delegate({ messenger, actions, events })
+ .then(() => setDelegated(true))
+ .catch(captureException);
+
+ return () => {
+ uiMessenger
+ .revoke({ messenger, actions, events })
+ .then(() => setDelegated(false))
+ .catch(captureException);
+ };
+ }, [uiMessenger, capabilities]);
+
+ const routeMessenger = routeMessengerRef.current;
+
+ // Wait for delegation to complete before rendering children, to ensure that
+ // the route messenger has access to the delegated capabilities when children
+ // are rendered.
+ if (!isDelegated) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/app/messengers/ui-messenger.test.ts b/app/messengers/ui-messenger.test.ts
new file mode 100644
index 00000000000..c8b53563092
--- /dev/null
+++ b/app/messengers/ui-messenger.test.ts
@@ -0,0 +1,298 @@
+import {
+ Messenger,
+ MOCK_ANY_NAMESPACE,
+ type MockAnyNamespace,
+} from '@metamask/messenger';
+import {
+ UIMessenger,
+ type UIMessengerActions,
+ type UIMessengerEvents,
+} from './ui-messenger';
+import Engine from '../core/Engine';
+
+jest.mock('../core/Engine', () => ({
+ controllerMessenger: {
+ call: jest.fn(),
+ subscribe: jest.fn(),
+ unsubscribe: jest.fn(),
+ },
+}));
+
+/**
+ * Create a delegatee messenger for testing. Uses MOCK_ANY_NAMESPACE to avoid
+ * namespace restrictions.
+ */
+function createDelegatee() {
+ return new Messenger(
+ { namespace: MOCK_ANY_NAMESPACE },
+ );
+}
+
+describe('UIMessenger', () => {
+ let uiMessenger: UIMessenger;
+ let mockCall: jest.Mock;
+ let mockSubscribe: jest.Mock;
+ let mockUnsubscribe: jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ uiMessenger = new UIMessenger();
+ mockCall = jest.mocked(Engine.controllerMessenger.call);
+ mockSubscribe = jest.mocked(Engine.controllerMessenger.subscribe);
+ mockUnsubscribe = jest.mocked(Engine.controllerMessenger.unsubscribe);
+ });
+
+ describe('delegate', () => {
+ describe('actions', () => {
+ it('registers a handler on the delegatee that routes to the background', async () => {
+ const delegatee = createDelegatee();
+ mockCall.mockReturnValue('result');
+
+ await uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+
+ const result = await delegatee.call(
+ 'SnapController:installSnaps',
+ 'metamask',
+ {},
+ );
+
+ expect(mockCall).toHaveBeenCalledWith(
+ 'SnapController:installSnaps',
+ 'metamask',
+ {},
+ );
+ expect(result).toBe('result');
+ });
+
+ it('throws if an excluded action is called', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ // @ts-expect-error: Excluded action for testing purposes.
+ actions: ['KeyringController:addNewKeyring'],
+ messenger: delegatee,
+ });
+
+ // @ts-expect-error: Excluded action for testing purposes.
+ expect(() => delegatee.call('KeyringController:addNewKeyring')).toThrow(
+ `The action "KeyringController:addNewKeyring" has not been exposed to the UI.`,
+ );
+ });
+
+ it('throws if the same action is delegated to the same messenger twice', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+
+ await expect(
+ uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ }),
+ ).rejects.toThrow(
+ `The action "${'SnapController:installSnaps'}" has already been delegated to this messenger.`,
+ );
+ });
+
+ it('allows the same action to be delegated to different messengers', async () => {
+ const delegatee1 = createDelegatee();
+ const delegatee2 = createDelegatee();
+
+ await expect(
+ Promise.all([
+ uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee1,
+ }),
+ uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee2,
+ }),
+ ]),
+ ).resolves.not.toThrow();
+ });
+ });
+
+ describe('events', () => {
+ it('subscribes to the event via Engine.controllerMessenger', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+
+ expect(mockSubscribe).toHaveBeenCalledWith(
+ 'SnapController:snapInstalled',
+ expect.any(Function),
+ );
+ });
+
+ it('publishes background events to the delegatee', async () => {
+ const delegatee = createDelegatee();
+ const handler = jest.fn();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (delegatee as any).subscribe('SnapController:snapInstalled', handler);
+
+ let capturedCallback!: (...args: unknown[]) => void;
+ mockSubscribe.mockImplementation(
+ (_event: string, callback: unknown) => {
+ capturedCallback = callback as (...args: unknown[]) => void;
+ },
+ );
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+
+ capturedCallback();
+
+ expect(handler).toHaveBeenCalled();
+ });
+
+ it('throws if the same event is delegated to the same messenger twice', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+
+ await expect(
+ uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ }),
+ ).rejects.toThrow(
+ `The event 'SnapController:snapInstalled' has already been delegated to this messenger`,
+ );
+ });
+
+ it('creates independent background subscriptions for different messengers', async () => {
+ const delegatee1 = createDelegatee();
+ const delegatee2 = createDelegatee();
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee1,
+ });
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee2,
+ });
+
+ expect(mockSubscribe).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
+
+ describe('revoke', () => {
+ describe('actions', () => {
+ it('unregisters the action handler from the delegatee', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+ await uiMessenger.revoke({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+
+ expect(() =>
+ delegatee.call('SnapController:installSnaps', 'metamask', {}),
+ ).toThrow();
+ });
+
+ it('silently ignores actions that have not been delegated', async () => {
+ const delegatee = createDelegatee();
+
+ await expect(
+ uiMessenger.revoke({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ }),
+ ).resolves.not.toThrow();
+ });
+
+ it('allows re-delegation after revoking', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+ await uiMessenger.revoke({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ });
+
+ await expect(
+ uiMessenger.delegate({
+ actions: ['SnapController:installSnaps'],
+ messenger: delegatee,
+ }),
+ ).resolves.not.toThrow();
+ });
+ });
+
+ describe('events', () => {
+ it('calls Engine.controllerMessenger.unsubscribe for the event', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+ await uiMessenger.revoke({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+
+ expect(mockUnsubscribe).toHaveBeenCalledWith(
+ 'SnapController:snapInstalled',
+ expect.any(Function),
+ );
+ });
+
+ it('allows re-delegation after revoking', async () => {
+ const delegatee = createDelegatee();
+
+ await uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+ await uiMessenger.revoke({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ });
+
+ await expect(
+ uiMessenger.delegate({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ }),
+ ).resolves.not.toThrow();
+ });
+
+ it('silently ignores events that have not been delegated', async () => {
+ const delegatee = createDelegatee();
+
+ await expect(
+ uiMessenger.revoke({
+ events: ['SnapController:snapInstalled'],
+ messenger: delegatee,
+ }),
+ ).resolves.not.toThrow();
+ });
+ });
+ });
+});
diff --git a/app/messengers/ui-messenger.ts b/app/messengers/ui-messenger.ts
new file mode 100644
index 00000000000..aa74718945f
--- /dev/null
+++ b/app/messengers/ui-messenger.ts
@@ -0,0 +1,323 @@
+import {
+ Messenger,
+ ActionConstraint,
+ ExtractEventPayload,
+ EventConstraint,
+ MessengerActions,
+ MessengerEvents,
+ ExtractActionParameters,
+ ExtractActionResponse,
+ ExtractEventHandler,
+} from '@metamask/messenger';
+import type { Json } from '@metamask/utils';
+import { MESSENGERS_WITH_EXCLUSIONS } from './configs';
+import Engine, { GlobalActions, GlobalEvents } from '../core/Engine';
+
+/**
+ * All actions are made asynchronous to match the implementation of the
+ * extension, which routes all actions through the background connection. This
+ * type makes a function asynchronous.
+ */
+type MakeAsynchronous unknown> =
+ InputFunction extends (...args: infer Args) => infer Return
+ ? (...args: Args) => Promise>
+ : never;
+
+/**
+ * All actions are made asynchronous to match the implementation of the
+ * extension, which routes all actions through the background connection. This
+ * type makes the given actions asynchronous.
+ */
+// Use a conditional type to ensure that TypeScript distributes a given union
+// and we get a union back (this is essentially a `map`)
+type MakeActionsAsynchronous = Action extends ActionConstraint
+ ? { type: Action['type']; handler: MakeAsynchronous }
+ : never;
+
+const EXCLUDED_ACTIONS = MESSENGERS_WITH_EXCLUSIONS.flatMap(
+ (config) => config.EXCLUDED_CAPABILITIES.actions,
+);
+
+type MessengerWithExclusions = (typeof MESSENGERS_WITH_EXCLUSIONS)[number];
+type ExcludedActionTypes =
+ MessengerWithExclusions['EXCLUDED_CAPABILITIES']['actions'][number];
+type ExcludedEventTypes =
+ MessengerWithExclusions['EXCLUDED_CAPABILITIES']['events'][number];
+
+/**
+ * Keeps only actions whose parameters and return value are JSON-serializable,
+ * to match the implementation of the extension.
+ */
+type WithJsonParams =
+ Action extends ActionConstraint
+ ? Parameters extends Json[]
+ ? Action
+ : never
+ : never;
+
+/**
+ * Keeps only events whose payload is JSON-serializable, to match the
+ * implementation of the extension.
+ */
+type WithJsonPayload = Event extends {
+ payload: infer P extends unknown[];
+}
+ ? P extends Json[]
+ ? Event
+ : never
+ : never;
+
+export type UIMessengerActions = MakeActionsAsynchronous<
+ WithJsonParams>
+>;
+
+export type UIMessengerEvents = WithJsonPayload<
+ Exclude
+>;
+
+/**
+ * A messenger that actions and/or events can be delegated to.
+ *
+ * This is a minimal type interface to avoid complex incompatibilities resulting
+ * from generics over invariant types.
+ */
+type DelegatedMessenger = Pick<
+ // The type is broadened to all actions/events because some messenger methods
+ // are contravariant over this type (`registerDelegatedActionHandler` and
+ // `publishDelegated` for example). If this type is narrowed to just the
+ // delegated actions/events, the types for event payload and action parameters
+ // would not be wide enough.
+ Messenger,
+ | '_internalPublishDelegated'
+ | '_internalRegisterDelegatedActionHandler'
+ | '_internalUnregisterDelegatedActionHandler'
+ | 'captureException'
+>;
+
+// We intentionally don't extend the base `Messenger` class here because the UI
+// messenger doesn't need to implement the full messenger API. It's only used as
+// a bridge between the root messenger and the route messengers, and it
+// delegates all of its actions and events to the root messenger by default, so
+// it doesn't need to implement the full API itself.
+export class UIMessenger {
+ /**
+ * The set of messengers we've delegated actions to, by action type.
+ */
+ readonly #actionDelegationTargets = new Map<
+ UIMessengerActions['type'],
+ Set
+ >();
+
+ /**
+ * The set of messengers we've delegated events to and their event handlers, by event type.
+ */
+ readonly #subscriptionDelegationTargets = new Map<
+ UIMessengerEvents['type'],
+ Map Promise>
+ >();
+
+ /**
+ * Delegate actions and events to a given messenger.
+ *
+ * Actions will be delegated by registering a handler on the delegatee
+ * messenger that submits a request to the background for the given action
+ * type and parameters. Events will be delegated by subscribing to the event
+ * on the root messenger and publishing it on the delegatee messenger when it
+ * occurs.
+ *
+ * @param options - The delegation options.
+ * @param options.actions - The action types to delegate.
+ * @param options.events - The event types to delegate.
+ * @param options.messenger - The messenger to delegate to.
+ * @throws If any action type has already been delegated to this messenger, or
+ * if any event type has already been delegated to this messenger.
+ */
+ async delegate<
+ Delegatee extends Messenger,
+ DelegatedActions extends (MessengerActions['type'] &
+ UIMessengerActions['type'])[],
+ DelegatedEvents extends (MessengerEvents['type'] &
+ UIMessengerEvents['type'])[],
+ >({
+ actions,
+ events,
+ messenger,
+ }: {
+ actions?: DelegatedActions;
+ events?: DelegatedEvents;
+ messenger: Delegatee;
+ }): Promise {
+ for (const actionType of actions ?? []) {
+ const delegatedActionHandler = (
+ ...args: ExtractActionParameters<
+ MessengerActions & UIMessengerActions,
+ typeof actionType
+ >
+ ): ExtractActionResponse<
+ MessengerActions & UIMessengerActions,
+ typeof actionType
+ > => {
+ const excludedActions: string[] = EXCLUDED_ACTIONS;
+ if (excludedActions.includes(actionType)) {
+ throw new Error(
+ `The action "${actionType}" has not been exposed to the UI.`,
+ );
+ }
+
+ return Engine.controllerMessenger.call(
+ actionType,
+ ...args,
+ ) as ExtractActionResponse<
+ MessengerActions & UIMessengerActions,
+ typeof actionType
+ >;
+ };
+
+ let delegationTargets = this.#actionDelegationTargets.get(actionType);
+ if (!delegationTargets) {
+ delegationTargets = new Set();
+ this.#actionDelegationTargets.set(actionType, delegationTargets);
+ }
+
+ if (delegationTargets.has(messenger)) {
+ throw new Error(
+ `The action "${actionType}" has already been delegated to this messenger.`,
+ );
+ }
+
+ delegationTargets.add(messenger);
+
+ // Intentionally calling a "deprecated" method here, since this is the
+ // internal API for delegation.
+ messenger._internalRegisterDelegatedActionHandler(
+ actionType,
+ delegatedActionHandler,
+ );
+ }
+
+ const promises = (events ?? []).map(async (eventType) => {
+ const untypedSubscriber = (
+ ...payload: ExtractEventPayload<
+ MessengerEvents & UIMessengerEvents,
+ typeof eventType
+ >
+ ): void => {
+ // Intentionally calling a "deprecated" method here, since this is the
+ // internal API for delegation.
+ messenger._internalPublishDelegated(eventType, ...payload);
+ };
+
+ // Cast to get more specific subscriber type for this specific event.
+ // The types get collapsed here to the type union of all delegated
+ // events, rather than the single subscriber type corresponding to this
+ // event.
+ const subscriber = untypedSubscriber as ExtractEventHandler<
+ MessengerEvents & UIMessengerEvents,
+ typeof eventType
+ >;
+
+ let delegatedEventSubscriptions =
+ this.#subscriptionDelegationTargets.get(eventType);
+
+ if (!delegatedEventSubscriptions) {
+ delegatedEventSubscriptions = new Map();
+ this.#subscriptionDelegationTargets.set(
+ eventType,
+ delegatedEventSubscriptions,
+ );
+ }
+
+ if (delegatedEventSubscriptions.has(messenger)) {
+ throw new Error(
+ `The event '${eventType}' has already been delegated to this messenger`,
+ );
+ }
+
+ Engine.controllerMessenger.subscribe(eventType, subscriber);
+ const unsubscribe = async () =>
+ Engine.controllerMessenger.unsubscribe(eventType, subscriber);
+
+ delegatedEventSubscriptions.set(messenger, unsubscribe);
+ });
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Revoke delegation of actions and events to a given messenger.
+ *
+ * This is essentially the inverse of `delegate`, and will remove the
+ * delegated action handlers and event subscriptions that were added in
+ * `delegate`.
+ *
+ * @param options - The revocation options.
+ * @param options.actions - The action types to revoke delegation for.
+ * @param options.events - The event types to revoke delegation for.
+ * @param options.messenger - The messenger to revoke delegation from.
+ * @returns A promise that resolves when the revocation is complete.
+ */
+ async revoke<
+ Delegatee extends Messenger,
+ DelegatedActions extends (MessengerActions['type'] &
+ UIMessengerActions['type'])[],
+ DelegatedEvents extends (MessengerEvents['type'] &
+ UIMessengerEvents['type'])[],
+ >({
+ actions,
+ events,
+ messenger,
+ }: {
+ actions?: DelegatedActions;
+ events?: DelegatedEvents;
+ messenger: Delegatee;
+ }): Promise {
+ for (const actionType of actions ?? []) {
+ const delegationTargets = this.#actionDelegationTargets.get(actionType);
+ if (!delegationTargets?.has(messenger)) {
+ // Nothing to revoke.
+ continue;
+ }
+
+ // Intentionally calling a "deprecated" method here, since this is the
+ // internal API for delegation.
+ messenger._internalUnregisterDelegatedActionHandler(actionType);
+ delegationTargets.delete(messenger);
+
+ if (delegationTargets.size === 0) {
+ this.#actionDelegationTargets.delete(actionType);
+ }
+ }
+
+ for (const eventType of events ?? []) {
+ const delegationTargets =
+ this.#subscriptionDelegationTargets.get(eventType);
+
+ if (!delegationTargets) {
+ // Nothing to revoke.
+ continue;
+ }
+
+ const unsubscribe = delegationTargets.get(messenger);
+ if (!unsubscribe) {
+ // Nothing to revoke.
+ continue;
+ }
+
+ await unsubscribe();
+ delegationTargets.delete(messenger);
+
+ if (delegationTargets.size === 0) {
+ this.#subscriptionDelegationTargets.delete(eventType);
+ }
+ }
+ }
+}
+
+/**
+ * Factory function to create a UI messenger instance.
+ *
+ * @returns A new instance of the UI messenger.
+ */
+export function createUIMessenger(): UIMessenger {
+ return new UIMessenger();
+}
diff --git a/app/util/bridge/hooks/useSubmitBridgeTx.ts b/app/util/bridge/hooks/useSubmitBridgeTx.ts
index 05f0bed0486..59a67b5d280 100644
--- a/app/util/bridge/hooks/useSubmitBridgeTx.ts
+++ b/app/util/bridge/hooks/useSubmitBridgeTx.ts
@@ -3,6 +3,7 @@ import type {
QuoteMetadata,
QuoteResponse,
} from '@metamask/bridge-controller';
+import type { BridgeStatusController } from '@metamask/bridge-status-controller';
import Engine from '../../../core/Engine';
import { useSelector } from 'react-redux';
import { selectShouldUseSmartTransaction } from '../../../selectors/smartTransactionsController';
@@ -139,7 +140,9 @@ export default function useSubmitBridgeTx() {
// check whether quoteResponse is an intent transaction
if (quoteResponse.quote.intent) {
return await Engine.context.BridgeStatusController.submitIntent({
- quoteResponse,
+ quoteResponse: quoteResponse as Parameters<
+ BridgeStatusController['submitIntent']
+ >[0]['quoteResponse'],
accountAddress: walletAddress,
location,
abTests,
@@ -152,7 +155,7 @@ export default function useSubmitBridgeTx() {
{
...quoteResponse,
approval: quoteResponse.approval ?? undefined,
- },
+ } as Parameters[1],
stxEnabled,
undefined, // quotesReceivedContext
location,
diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json
index b646f603d97..26b17517592 100644
--- a/app/util/test/initial-background-state.json
+++ b/app/util/test/initial-background-state.json
@@ -242,7 +242,7 @@
"blockExplorerUrls": [],
"chainId": "0x8f",
"defaultRpcEndpointIndex": 0,
- "name": "Monad Mainnet",
+ "name": "Monad",
"nativeCurrency": "MON",
"rpcEndpoints": [
{
diff --git a/app/util/test/mock-route-messenger.ts b/app/util/test/mock-route-messenger.ts
new file mode 100644
index 00000000000..8635abf8037
--- /dev/null
+++ b/app/util/test/mock-route-messenger.ts
@@ -0,0 +1,50 @@
+import {
+ ActionHandler,
+ Messenger,
+ MOCK_ANY_NAMESPACE,
+ MockAnyNamespace,
+} from '@metamask/messenger';
+import {
+ UIMessengerActions,
+ UIMessengerEvents,
+} from '../../messengers/ui-messenger';
+import { RouteMessenger } from '../../messengers/route-messenger';
+
+/**
+ * Create a route messenger for testing with the specified action handlers.
+ *
+ * This creates a real Messenger instance with a fake namespace and registers
+ * the provided action handlers on it.
+ *
+ * @param actionHandlers - A map of action type strings to handler functions.
+ * @returns A messenger with the specified action handlers registered.
+ * @example
+ * ```typescript
+ * const addNetwork = jest.fn().mockResolvedValue({ chainId: '0x1' });
+ * const messenger = createMockRouteMessenger({
+ * 'NetworkController:addNetwork': addNetwork,
+ * });
+ * ```
+ */
+export function createMockRouteMessenger<
+ Actions extends UIMessengerActions = never,
+>(actionHandlers?: {
+ [Action in Actions as Action['type']]: Action['handler'];
+}): RouteMessenger {
+ const messenger = new Messenger<
+ MockAnyNamespace,
+ UIMessengerActions,
+ UIMessengerEvents
+ >({
+ namespace: MOCK_ANY_NAMESPACE,
+ });
+
+ for (const [actionType, handler] of Object.entries(actionHandlers ?? {})) {
+ messenger.registerActionHandler(
+ actionType as Actions['type'],
+ handler as ActionHandler,
+ );
+ }
+
+ return messenger;
+}
diff --git a/app/util/test/mock-ui-messenger.ts b/app/util/test/mock-ui-messenger.ts
new file mode 100644
index 00000000000..fea4eb76066
--- /dev/null
+++ b/app/util/test/mock-ui-messenger.ts
@@ -0,0 +1,71 @@
+import {
+ ActionConstraint,
+ ActionHandler,
+ EventConstraint,
+ Messenger,
+} from '@metamask/messenger';
+import type {
+ UIMessenger,
+ UIMessengerActions,
+ UIMessengerEvents,
+} from '../../messengers/ui-messenger';
+
+type DelegateeMessenger = Messenger;
+
+interface DelegateArgs<
+ Actions extends UIMessengerActions,
+ Events extends UIMessengerEvents,
+> {
+ actions?: Actions['type'][];
+ events?: Events['type'][];
+ messenger: DelegateeMessenger;
+}
+
+/**
+ * Create a UI messenger for testing with the specified action handlers.
+ *
+ * This creates a real Messenger instance with a fake namespace and registers
+ * the provided action handlers on it.
+ *
+ * @param actionHandlers - A map of action type strings to handler functions.
+ * @returns A messenger with the specified action handlers registered.
+ * @example
+ * ```typescript
+ * const addNetwork = jest.fn().mockResolvedValue({ chainId: '0x1' });
+ * const messenger = createMockUIMessenger({
+ * 'NetworkController:addNetwork': addNetwork,
+ * });
+ * ```
+ */
+export function createMockUIMessenger<
+ Actions extends UIMessengerActions = never,
+ Events extends UIMessengerEvents = never,
+>(actionHandlers?: {
+ [Action in Actions as Action['type']]: Action['handler'];
+}): UIMessenger {
+ return {
+ async delegate({ actions = [], messenger }: DelegateArgs) {
+ for (const actionType of actions) {
+ const handler = actionHandlers?.[actionType];
+ if (!handler) {
+ throw new Error(
+ `No handler registered for action "${String(actionType)}".`,
+ );
+ }
+
+ messenger._internalRegisterDelegatedActionHandler(
+ actionType,
+ handler as ActionHandler,
+ );
+ }
+
+ // No background connection in tests — events are not subscribed to.
+ },
+
+ async revoke({ actions = [], messenger }: DelegateArgs) {
+ for (const actionType of actions) {
+ messenger._internalUnregisterDelegatedActionHandler(actionType);
+ }
+ },
+ } as unknown as UIMessenger;
+}
diff --git a/app/util/test/renderWithProvider.tsx b/app/util/test/renderWithProvider.tsx
index cd1d8b9929b..fe1b721096b 100644
--- a/app/util/test/renderWithProvider.tsx
+++ b/app/util/test/renderWithProvider.tsx
@@ -17,6 +17,11 @@ import { Theme } from '../theme/models';
import configureStore from './configureStore';
import { RootState } from '../../reducers';
import { FeatureFlagOverrideProvider } from '../../contexts/FeatureFlagOverrideContext';
+import { UIMessengerProvider } from '../../contexts/ui-messenger';
+import { createMockUIMessenger } from './mock-ui-messenger';
+import { RouteMessengerContext } from '../../contexts/route-messenger';
+import { RouteMessenger } from '../../messengers/route-messenger';
+import { UIMessenger } from '../../messengers/ui-messenger';
// DeepPartial is a generic type that recursively makes all properties of a given type T optional
export type DeepPartial = T extends (...args: unknown[]) => unknown
@@ -30,9 +35,12 @@ export type DeepPartial = T extends (...args: unknown[]) => unknown
{ [K in keyof T]?: DeepPartial }
: // Otherwise, return T or undefined.
T | undefined;
+
export interface ProviderValues {
state?: DeepPartial;
theme?: Theme;
+ uiMessenger?: UIMessenger;
+ routeMessenger?: RouteMessenger;
}
export default function renderWithProvider(
@@ -41,7 +49,13 @@ export default function renderWithProvider(
includeNavigationContainer = true,
includeFeatureFlagOverrideProvider = true,
) {
- const { state = {}, theme = mockTheme } = providerValues ?? {};
+ const {
+ state = {},
+ theme = mockTheme,
+ uiMessenger = createMockUIMessenger(),
+ routeMessenger = null,
+ } = providerValues ?? {};
+
const store = configureStore(state);
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
require('../../store')._updateMockState(state);
@@ -55,11 +69,22 @@ export default function renderWithProvider(
);
}
+
+ if (routeMessenger) {
+ wrappedChildren = (
+
+ {wrappedChildren}
+
+ );
+ }
+
return (
-
- {wrappedChildren}
-
+
+
+ {wrappedChildren}
+
+
);
};
@@ -106,13 +131,34 @@ export function renderHookWithProvider(
hook: (props: Props) => Result,
providerValues?: ProviderValues,
) {
- const { state = {} } = providerValues ?? {};
+ const {
+ state = {},
+ uiMessenger = createMockUIMessenger(),
+ routeMessenger = null,
+ } = providerValues ?? {};
+
const store = configureStore(state);
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
require('../../store')._updateMockState(state);
- const Providers = ({ children }: { children: React.ReactElement }) => (
- {children}
- );
+
+ const Providers = ({ children }: { children: React.ReactElement }) => {
+ let wrappedChildren = children;
+ if (routeMessenger) {
+ wrappedChildren = (
+
+ {wrappedChildren}
+
+ );
+ }
+
+ return (
+
+
+ {wrappedChildren}
+
+
+ );
+ };
return {
...renderHook(hook, { wrapper: Providers } as RenderHookOptions),
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 360d1dc79c9..7f57b5a5911 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1142,6 +1142,7 @@
"perps": {
"basic_functionality_disabled_title": "Perps is not available",
"title": "Perps",
+ "transfer_to_perps": "Transfer to Perps",
"perps_trading": "Perps trading",
"perp_account_balance": "Perp account balance",
"manage_balance": "Manage balance",
@@ -2308,10 +2309,10 @@
},
"predict": {
"title": "MetaMask Predictions",
+ "transfer_to_predictions": "Transfer to Predictions",
"select_predict_account": "Select predict account",
"search_account": "Search an account",
"home": {
- "portfolio_module_placeholder": "Portfolio module",
"live_now_title": "Live now",
"categories_placeholder": "Categories",
"popular_today_placeholder": "Popular today",
diff --git a/package.json b/package.json
index 50cc314d694..faa171efa5e 100644
--- a/package.json
+++ b/package.json
@@ -203,7 +203,6 @@
"@metamask/messenger@^0.3.0": "^1.0.0",
"@metamask/messenger": "^1.2.0",
"@metamask/keyring-internal-api": "^11.0.1",
- "@metamask/accounts-controller": "^38.0.0",
"@metamask/keyring-api@npm:^21.3.0": "23.1.0",
"@metamask/keyring-api@npm:^21.4.0": "23.1.0",
"@metamask/keyring-api@npm:^21.6.0": "23.1.0",
@@ -213,7 +212,8 @@
"@metamask/permission-controller": "^13.1.1",
"react-native-ble-plx@npm:3.4.0": "patch:react-native-ble-plx@npm%3A3.4.0#~/.yarn/patches/react-native-ble-plx-npm-3.4.0-401e8b3343.patch",
"@metamask/transaction-controller": "67.0.0",
- "@metamask/keyring-controller": "^26.0.0"
+ "@metamask/keyring-controller": "^26.0.0",
+ "@metamask/accounts-controller": "^39.0.0"
},
"dependencies": {
"@braze/react-native-sdk": "patch:@braze/react-native-sdk@npm%3A19.1.0#~/.yarn/patches/@braze-react-native-sdk-npm-19.1.0-076-reactmoduleinfo.patch",
@@ -232,8 +232,8 @@
"@ledgerhq/react-native-hw-transport-ble": "^6.37.0",
"@metamask/abi-utils": "^3.0.0",
"@metamask/account-api": "^1.0.4",
- "@metamask/account-tree-controller": "^7.2.0",
- "@metamask/accounts-controller": "^38.0.0",
+ "@metamask/account-tree-controller": "^7.5.0",
+ "@metamask/accounts-controller": "^39.0.0",
"@metamask/address-book-controller": "^7.1.2",
"@metamask/ai-controllers": "^0.7.0",
"@metamask/analytics-controller": "^1.0.0",
@@ -259,7 +259,7 @@
"@metamask/design-system-twrnc-preset": "^0.5.0",
"@metamask/design-tokens": "^8.5.0",
"@metamask/earn-controller": "^12.1.0",
- "@metamask/eip-5792-middleware": "^2.0.0",
+ "@metamask/eip-5792-middleware": "^3.0.4",
"@metamask/eip1193-permission-middleware": "^1.0.2",
"@metamask/ens-resolver-snap": "^1.2.0",
"@metamask/eth-hd-keyring": "^14.1.1",
@@ -285,9 +285,8 @@
"@metamask/keyring-api": "^23.1.0",
"@metamask/keyring-controller": "^26.0.0",
"@metamask/keyring-internal-api": "^11.0.1",
- "@metamask/keyring-sdk": "^2.0.2",
+ "@metamask/keyring-sdk": "^2.1.1",
"@metamask/keyring-snap-client": "^9.0.2",
- "@metamask/keyring-utils": "^3.2.0",
"@metamask/logging-controller": "^8.0.0",
"@metamask/message-signing-snap": "^1.1.2",
"@metamask/messenger": "^1.2.0",
@@ -297,7 +296,7 @@
"@metamask/money-account-balance-service": "^1.0.0",
"@metamask/money-account-controller": "^0.2.0",
"@metamask/money-account-upgrade-controller": "^2.0.0",
- "@metamask/multichain-account-service": "^8.0.1",
+ "@metamask/multichain-account-service": "^10.0.2",
"@metamask/multichain-api-client": "^0.10.1",
"@metamask/multichain-api-middleware": "^3.1.1",
"@metamask/multichain-network-controller": "^3.1.0",
@@ -332,6 +331,7 @@
"@metamask/signature-controller": "^39.2.0",
"@metamask/slip44": "^4.2.0",
"@metamask/smart-transactions-controller": "^24.2.0",
+ "@metamask/snap-account-service": "^0.3.0",
"@metamask/snaps-controllers": "^20.0.5",
"@metamask/snaps-execution-environments": "^11.0.2",
"@metamask/snaps-rpc-methods": "^16.0.0",
@@ -345,7 +345,7 @@
"@metamask/superstruct": "^3.2.1",
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/transaction-controller": "^67.0.0",
- "@metamask/transaction-pay-controller": "^23.3.0",
+ "@metamask/transaction-pay-controller": "^23.5.0",
"@metamask/tron-wallet-snap": "^1.25.6",
"@metamask/utils": "^11.11.0",
"@metamask/wallet": "^2.0.0",
diff --git a/tests/component-view/mocks.ts b/tests/component-view/mocks.ts
index 5509ee380e9..0cfc00a1b27 100644
--- a/tests/component-view/mocks.ts
+++ b/tests/component-view/mocks.ts
@@ -362,6 +362,7 @@ jest.mock('../../app/core/Engine', () => {
trackMarketDetailsOpened: jest.fn(),
trackGeoBlockTriggered: jest.fn(),
trackActivityViewed: jest.fn(),
+ trackSearchInteracted: jest.fn(),
trackPortfolioPositionsButtonTapped: jest.fn(),
trackPortfolioTransactionInitiated: jest.fn(),
trackPositionsScreenViewed: jest.fn(),
diff --git a/tests/framework/Assertions.ts b/tests/framework/Assertions.ts
index 064d01b2ebc..e48a8aa4df2 100644
--- a/tests/framework/Assertions.ts
+++ b/tests/framework/Assertions.ts
@@ -2,6 +2,10 @@ import { waitFor } from 'detox';
import Utilities, { BASE_DEFAULTS } from './Utilities.ts';
import { AssertionOptions } from './types.ts';
import Matchers from './Matchers.ts';
+import {
+ asDetoxElement,
+ type EncapsulatedElementType,
+} from './EncapsulatedElement.ts';
import { Json } from '@metamask/utils';
/**
@@ -12,11 +16,12 @@ export default class Assertions {
* Assert element is visible with auto-retry
*/
static async expectElementToBeVisible(
- detoxElement:
+ elem:
| DetoxElement
| WebElement
| DetoxMatcher
- | IndexableNativeElement,
+ | IndexableNativeElement
+ | EncapsulatedElementType,
options: AssertionOptions = {},
): Promise {
const {
@@ -26,7 +31,9 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
- const el = await detoxElement;
+ const el = (await elem) as Awaited<
+ DetoxElement | WebElement | DetoxMatcher | IndexableNativeElement
+ >;
const isWebElement = Utilities.isWebElement(el);
if (isWebElement) {
// eslint-disable-next-line jest/valid-expect, @typescript-eslint/no-explicit-any
@@ -48,11 +55,12 @@ export default class Assertions {
* Assert element is not visible with auto-retry
*/
static async expectElementToNotBeVisible(
- detoxElement:
+ elem:
| DetoxElement
| WebElement
| DetoxMatcher
- | IndexableNativeElement,
+ | IndexableNativeElement
+ | EncapsulatedElementType,
options: AssertionOptions = {},
): Promise {
const {
@@ -62,7 +70,9 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
- const el = await detoxElement;
+ const el = (await elem) as Awaited<
+ DetoxElement | WebElement | DetoxMatcher | IndexableNativeElement
+ >;
const isWebElement = Utilities.isWebElement(el);
if (isWebElement) {
// eslint-disable-next-line jest/valid-expect, @typescript-eslint/no-explicit-any
@@ -82,7 +92,7 @@ export default class Assertions {
* Assert element has specific text with auto-retry
*/
static async expectElementToHaveText(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
text: string,
options: AssertionOptions = {},
): Promise {
@@ -93,7 +103,7 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
await waitFor(el).toHaveText(text).withTimeout(100);
},
{
@@ -142,7 +152,7 @@ export default class Assertions {
* Assert element does not have specific text with auto-retry
*/
static async expectElementToNotHaveText(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
text: string,
options: AssertionOptions = {},
): Promise {
@@ -153,7 +163,7 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
await waitFor(el).not.toHaveText(text).withTimeout(100);
},
{
@@ -167,7 +177,7 @@ export default class Assertions {
* Assert element has specific label with auto-retry
*/
static async expectElementToHaveLabel(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
label: string,
options: AssertionOptions = {},
): Promise {
@@ -178,7 +188,7 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
await waitFor(el).toHaveLabel(label).withTimeout(100);
},
{
@@ -201,12 +211,17 @@ export default class Assertions {
return Utilities.executeWithRetry(
async () => {
const textElement = allowDuplicates
- ? (await Matchers.getElementByText(text)).atIndex(0)
- : await Matchers.getElementByText(text);
+ ? Matchers.getElementByText(text, 0)
+ : Matchers.getElementByText(text);
+ const el = await asDetoxElement(textElement);
if (device.getPlatform() === 'ios') {
- await waitFor(textElement).toExist().withTimeout(100);
+ await waitFor(el as Detox.NativeElement)
+ .toExist()
+ .withTimeout(100);
} else {
- await waitFor(textElement).toBeVisible().withTimeout(100);
+ await waitFor(el as Detox.NativeElement)
+ .toBeVisible()
+ .withTimeout(100);
}
},
{
@@ -228,11 +243,15 @@ export default class Assertions {
const { timeout = BASE_DEFAULTS.timeout } = options;
return Utilities.executeWithRetry(
async () => {
- const textElement = await Matchers.getElementByText(text);
+ const el = await asDetoxElement(Matchers.getElementByText(text));
if (device.getPlatform() === 'ios') {
- await waitFor(textElement).not.toExist().withTimeout(100);
+ await waitFor(el as Detox.NativeElement)
+ .not.toExist()
+ .withTimeout(100);
} else {
- await waitFor(textElement).not.toBeVisible().withTimeout(100);
+ await waitFor(el as Detox.NativeElement)
+ .not.toBeVisible()
+ .withTimeout(100);
}
},
{
@@ -246,7 +265,7 @@ export default class Assertions {
* Assert element is enabled with auto-retry
*/
static async expectToggleToBeOn(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: AssertionOptions = {},
): Promise {
const {
@@ -258,7 +277,7 @@ export default class Assertions {
async () => {
try {
const el = (await Utilities.waitForReadyState(
- detoxElement,
+ elem,
)) as Detox.IndexableNativeElement;
// eslint-disable-next-line jest/valid-expect, @typescript-eslint/no-explicit-any
await (expect(el) as any).toHaveToggleValue(true);
@@ -284,7 +303,7 @@ export default class Assertions {
* Assert element is disabled with auto-retry
*/
static async expectToggleToBeOff(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: AssertionOptions = {},
): Promise {
const {
@@ -296,7 +315,7 @@ export default class Assertions {
async () => {
try {
const el = (await Utilities.waitForReadyState(
- detoxElement,
+ elem,
)) as Detox.IndexableNativeElement;
// eslint-disable-next-line jest/valid-expect, @typescript-eslint/no-explicit-any
await (expect(el) as any).toHaveToggleValue(false);
@@ -326,7 +345,7 @@ export default class Assertions {
throw new Error('Both actual and expected text must be provided');
}
- return expect(actualText).toBe(expectedText);
+ expect(actualText).toBe(expectedText);
} catch (error) {
if (actualText !== expectedText) {
throw new Error(
@@ -541,19 +560,21 @@ export default class Assertions {
* @deprecated Use expectElementToBeVisible() instead for better error handling and retry mechanisms
*/
static async checkIfVisible(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
timeout = 15000,
): Promise {
- return this.expectElementToBeVisible(detoxElement, { timeout });
+ return this.expectElementToBeVisible(elem, { timeout });
}
/**
* Legacy method: Check if a web element exists
* @deprecated Use expectElementToBeVisible() instead for better error handling and retry mechanisms
*/
- static async webViewElementExists(detoxElement: DetoxElement): Promise {
+ static async webViewElementExists(
+ elem: EncapsulatedElementType,
+ ): Promise {
// For web elements, just use the basic expect assertion
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
// Use Detox's expect which has toExist method
// Use Detox's expect syntax for element existence
// eslint-disable-next-line @typescript-eslint/no-explicit-any, jest/valid-expect
@@ -565,10 +586,10 @@ export default class Assertions {
* @deprecated Use expectElementToNotBeVisible() instead for better error handling and retry mechanisms
*/
static async checkIfNotVisible(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
timeout = 15000,
): Promise {
- return this.expectElementToNotBeVisible(detoxElement as DetoxElement, {
+ return this.expectElementToNotBeVisible(elem as DetoxElement, {
timeout,
});
}
@@ -578,11 +599,11 @@ export default class Assertions {
* @deprecated Use expectElementToHaveText() instead for better error handling and retry mechanisms
*/
static async checkIfElementToHaveText(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
text: string,
timeout = 15000,
): Promise {
- return this.expectElementToHaveText(detoxElement as DetoxElement, text, {
+ return this.expectElementToHaveText(elem as DetoxElement, text, {
timeout,
});
}
@@ -592,11 +613,11 @@ export default class Assertions {
* @deprecated Use expectElementToHaveLabel() instead for better error handling and retry mechanisms
*/
static async checkIfElementHasLabel(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
label: string,
timeout = 15000,
): Promise {
- return this.expectElementToHaveLabel(detoxElement, label, { timeout });
+ return this.expectElementToHaveLabel(elem, label, { timeout });
}
/**
@@ -620,8 +641,10 @@ export default class Assertions {
): Promise {
return Utilities.executeWithRetry(
async () => {
- const textElement = await Matchers.getElementByText(text);
- await waitFor(textElement).not.toBeVisible().withTimeout(100);
+ const el = await asDetoxElement(Matchers.getElementByText(text));
+ await waitFor(el as Detox.NativeElement)
+ .not.toBeVisible()
+ .withTimeout(100);
},
{
timeout,
@@ -635,13 +658,13 @@ export default class Assertions {
* @deprecated Use expectElementToNotHaveText() or custom assertion instead for better error handling and retry mechanisms
*/
static async checkIfElementNotToHaveText(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
text: string,
timeout = 15000,
): Promise {
return Utilities.executeWithRetry(
async () => {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
await waitFor(el).not.toHaveText(text).withTimeout(100);
},
{
@@ -656,13 +679,13 @@ export default class Assertions {
* @deprecated Use expectElementToNotBeVisible() or custom assertion instead for better error handling and retry mechanisms
*/
static async checkIfElementDoesNotHaveLabel(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
label: string,
timeout = 15000,
): Promise {
return Utilities.executeWithRetry(
async () => {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
await waitFor(el).not.toHaveLabel(label).withTimeout(100);
},
{
@@ -676,8 +699,8 @@ export default class Assertions {
* Legacy method: Check if toggle is in "on" state
* @deprecated Use expectToggleToBeOn() instead for better error handling and retry mechanisms
*/
- static async checkIfToggleIsOn(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkIfToggleIsOn(elem: EncapsulatedElementType): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
// Use Detox's expect syntax for toggle values
// eslint-disable-next-line @typescript-eslint/no-explicit-any, jest/valid-expect
await (expect(el) as any).toHaveToggleValue(true);
@@ -687,8 +710,10 @@ export default class Assertions {
* Legacy method: Check if toggle is in "off" state
* @deprecated Use expectToggleToBeOff() instead for better error handling and retry mechanisms
*/
- static async checkIfToggleIsOff(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkIfToggleIsOff(
+ elem: EncapsulatedElementType,
+ ): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
// Use Detox's expect syntax for toggle values
// eslint-disable-next-line @typescript-eslint/no-explicit-any, jest/valid-expect
await (expect(el) as any).toHaveToggleValue(false);
@@ -698,8 +723,8 @@ export default class Assertions {
* Legacy method: Check if element is enabled
* @deprecated Use Utilities.waitForElementToBeEnabled() instead for better retry handling
*/
- static async checkIfEnabled(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkIfEnabled(elem: EncapsulatedElementType): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
const attributes = await el.getAttributes();
return 'enabled' in attributes ? !!attributes.enabled : false;
}
@@ -708,8 +733,10 @@ export default class Assertions {
* Legacy method: Check if element is disabled
* @deprecated Use Utilities.waitForElementToBeEnabled() with negated logic instead
*/
- static async checkIfDisabled(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkIfDisabled(
+ elem: EncapsulatedElementType,
+ ): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
const attributes = await el.getAttributes();
return 'enabled' in attributes ? !attributes.enabled : true;
}
diff --git a/tests/framework/EncapsulatedElement.ts b/tests/framework/EncapsulatedElement.ts
index 24bcc54b1b8..3bdf3f82a37 100644
--- a/tests/framework/EncapsulatedElement.ts
+++ b/tests/framework/EncapsulatedElement.ts
@@ -44,7 +44,7 @@ export interface PlatformLocator {
* }
*/
export interface LocatorConfig {
- detox?: () => DetoxElement;
+ detox?: () => EncapsulatedElementType;
appium?:
| (() => Promise)
| {
@@ -120,7 +120,7 @@ export class EncapsulatedElement {
}
// Execute the function to get the DetoxElement
- return config.detox();
+ return config.detox() as unknown as DetoxElement;
}
/**
@@ -137,7 +137,7 @@ export class EncapsulatedElement {
// If appium is a function, use it as a generic locator
if (typeof config.appium === 'function') {
- return config.appium();
+ return config.appium() as Promise;
}
// Otherwise, it's a platform-specific configuration
@@ -151,7 +151,7 @@ export class EncapsulatedElement {
}
// Execute the platform-specific function to get the Promise
- return platformLocator();
+ return platformLocator() as Promise;
}
}
@@ -175,9 +175,9 @@ export function encapsulated(config: LocatorConfig): EncapsulatedElementType {
* @returns PlaywrightElement
*/
export async function asPlaywrightElement(
- element: EncapsulatedElementType,
+ elem: EncapsulatedElementType,
): Promise {
- return (await element) as PlaywrightElement;
+ return (await elem) as PlaywrightElement;
}
/**
@@ -192,6 +192,6 @@ export async function asPlaywrightElement(
*
* @returns DetoxElement
*/
-export function asDetoxElement(element: EncapsulatedElementType): DetoxElement {
- return element as DetoxElement;
+export function asDetoxElement(elem: EncapsulatedElementType): DetoxElement {
+ return elem as DetoxElement;
}
diff --git a/tests/framework/Gestures.ts b/tests/framework/Gestures.ts
index 01f61a078d6..03dbc9c710a 100644
--- a/tests/framework/Gestures.ts
+++ b/tests/framework/Gestures.ts
@@ -11,6 +11,7 @@ import {
} from './types.ts';
import { createLogger } from './logger.ts';
import { sleep } from '../../app/util/testUtils';
+import { type EncapsulatedElementType } from './EncapsulatedElement.ts';
const logger = createLogger({ name: 'Gestures' });
@@ -24,7 +25,7 @@ export default class Gestures {
* @param options - Options for the tap action
*/
private static tapWithChecks = async (
- elem: DetoxElement | WebElement,
+ elem: DetoxElement | WebElement | EncapsulatedElementType,
options: {
checkStability?: boolean;
checkVisibility?: boolean;
@@ -83,7 +84,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWith
*/
static async tap(
- elem: DetoxElement | WebElement,
+ elem: DetoxElement | WebElement | EncapsulatedElementType,
options: TapOptions = {},
): Promise {
const {
@@ -120,7 +121,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWith
*/
static async waitAndTap(
- elem: DetoxElement | WebElement,
+ elem: DetoxElement | WebElement | EncapsulatedElementType,
options: TapOptions = {},
): Promise {
const {
@@ -156,7 +157,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWithRetry
*/
static async tapAtIndex(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
index: number,
options: TapOptions = {},
): Promise {
@@ -203,7 +204,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWithRetry
*/
static async tapAtPoint(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
point: { x: number; y: number },
options: TapOptions = {},
): Promise {
@@ -240,7 +241,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWithRetry
*/
static async dblTap(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
options: TapOptions = {},
): Promise {
const {
@@ -279,7 +280,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWithRetry
*/
static async longPress(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
options: LongPressOptions = {},
): Promise {
const {
@@ -319,7 +320,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWith
*/
static async typeText(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
text: string,
options: TypeTextOptions = {},
): Promise {
@@ -412,7 +413,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWithRetry
*/
static async replaceText(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
text: string,
options: GestureOptions = {},
): Promise {
@@ -453,7 +454,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWith
*/
static async swipe(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
direction: 'up' | 'down' | 'left' | 'right',
options: SwipeOptions = {},
): Promise {
@@ -501,7 +502,7 @@ export default class Gestures {
* @throws Will retry the operation if it fails, with retry logic handled by executeWith
*/
static async scrollToElement(
- targetElement: DetoxElement,
+ targetElement: DetoxElement | EncapsulatedElementType,
scrollableContainer: Promise,
options: ScrollOptions = {},
): Promise {
@@ -576,7 +577,7 @@ export default class Gestures {
* @deprecated Use longPress() instead for better error handling and retry mechanisms
*/
static async tapAndLongPress(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
timeout = 2000,
): Promise {
return this.longPress(elem, { duration: timeout });
@@ -608,7 +609,9 @@ export default class Gestures {
* Legacy method: Double tap an element
* @deprecated Use dblTap() instead for better error handling and retry mechanisms - we should replace the function name when we have migrated all usages
*/
- static async doubleTap(elem: DetoxElement): Promise {
+ static async doubleTap(
+ elem: DetoxElement | EncapsulatedElementType,
+ ): Promise {
return Utilities.executeWithRetry(
async () => {
const el = (await elem) as Detox.IndexableNativeElement;
@@ -625,7 +628,7 @@ export default class Gestures {
* @deprecated Use typeText() with clearFirst option or the replaceText() from Gestures.ts instead for better error handling and retry mechanisms
*/
static async clearField(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
options: GestureOptions = {},
): Promise {
const {
@@ -663,7 +666,7 @@ export default class Gestures {
* @deprecated Use typeText() with hideKeyboard option instead for better error handling and retry mechanisms
*/
static async typeTextAndHideKeyboard(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
text: string,
): Promise {
return this.typeText(elem, text, {
@@ -677,7 +680,7 @@ export default class Gestures {
* @deprecated Use typeText() with hideKeyboard: false option instead for better error handling and retry mechanisms
*/
static async typeTextWithoutKeyboard(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
text: string,
): Promise {
return this.typeText(elem, text, {
@@ -691,7 +694,7 @@ export default class Gestures {
* @deprecated Use replaceText() instead for better error handling and retry mechanisms
*/
static async replaceTextInField(
- elem: DetoxElement,
+ elem: DetoxElement | EncapsulatedElementType,
text: string,
timeout = 10000,
): Promise {
diff --git a/tests/framework/Matchers.ts b/tests/framework/Matchers.ts
index 42c08715465..649b6f6f34d 100644
--- a/tests/framework/Matchers.ts
+++ b/tests/framework/Matchers.ts
@@ -1,33 +1,38 @@
import { web, system } from 'detox';
+import { type EncapsulatedElementType } from './EncapsulatedElement.ts';
+import { resolve } from './Selector.ts';
/**
* Utility class for matching (locating) UI elements
*/
export default class Matchers {
/**
- * Get element by ID with optional index
+ * Get element by ID with optional index.
*/
- static async getElementByID(
+ static getElementByID(
elementId: string | RegExp,
index?: number,
- ): Promise {
- const el = element(by.id(elementId));
- if (index !== undefined) {
- return el.atIndex(index) as Detox.IndexableNativeElement;
+ ): EncapsulatedElementType {
+ if (typeof elementId === 'string') {
+ return resolve({ testID: elementId, index });
}
- return el as Detox.IndexableNativeElement;
+ const el = element(by.id(elementId));
+ return (index !== undefined
+ ? el.atIndex(index)
+ : el) as unknown as DetoxElement;
}
/**
- * Get element by text with optional index
+ * Get element by text with optional index.
*/
- static async getElementByText(
+ static getElementByText(
text: string | RegExp,
index = 0,
- ): Promise {
- return element(by.text(text)).atIndex(
- index,
- ) as Detox.IndexableNativeElement;
+ ): EncapsulatedElementType {
+ if (typeof text === 'string') {
+ return resolve({ text, index });
+ }
+ return element(by.text(text)).atIndex(index) as unknown as DetoxElement;
}
/**
@@ -39,7 +44,9 @@ export default class Matchers {
): Promise {
const escaped = containsText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(escaped, 'i');
- return this.getElementByText(pattern, index);
+ return element(by.text(pattern)).atIndex(
+ index,
+ ) as Detox.IndexableNativeElement;
}
/**
@@ -60,13 +67,11 @@ export default class Matchers {
/**
* Get element by label (accessibility label on iOS, content description on Android)
*/
- static async getElementByLabel(
+ static getElementByLabel(
label: string,
- index = 0,
- ): Promise {
- return element(by.label(label)).atIndex(
- index,
- ) as Detox.IndexableNativeElement;
+ index?: number,
+ ): EncapsulatedElementType {
+ return resolve({ label, index });
}
/**
diff --git a/tests/framework/Utilities.ts b/tests/framework/Utilities.ts
index 873b2622746..7bf12c8923f 100644
--- a/tests/framework/Utilities.ts
+++ b/tests/framework/Utilities.ts
@@ -1,6 +1,7 @@
import { waitFor } from 'detox';
import { blacklistURLs } from '../resources/blacklistURLs.json';
import { RetryOptions, StabilityOptions } from './types.ts';
+import { type EncapsulatedElementType } from './EncapsulatedElement.ts';
import { createLogger } from './logger.ts';
// eslint-disable-next-line import-x/no-nodejs-modules
import { setTimeout as asyncSetTimeout } from 'node:timers/promises';
@@ -39,8 +40,10 @@ export default class Utilities {
/**
* Check if element is enabled (non-retry version)
*/
- static async checkElementEnabled(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkElementEnabled(
+ elem: EncapsulatedElementType,
+ ): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
const attributes = await el.getAttributes();
if (!('enabled' in attributes) || !attributes.enabled) {
throw new Error(
@@ -57,8 +60,10 @@ export default class Utilities {
}
}
- static async checkElementDisabled(detoxElement: DetoxElement): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ static async checkElementDisabled(
+ elem: EncapsulatedElementType,
+ ): Promise {
+ const el = (await elem) as Detox.IndexableNativeElement;
const attributes = await el.getAttributes();
if (!('enabled' in attributes) || attributes.enabled) {
throw new Error('🚫 Element is enabled, but should be disabled.');
@@ -69,11 +74,11 @@ export default class Utilities {
* Wait for element to be enabled with retry mechanism
*/
static async waitForElementToBeEnabled(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
timeout = 3500,
interval = 100,
): Promise {
- return this.executeWithRetry(() => this.checkElementEnabled(detoxElement), {
+ return this.executeWithRetry(() => this.checkElementEnabled(elem), {
timeout,
interval,
description: 'Element to be enabled',
@@ -85,10 +90,10 @@ export default class Utilities {
* Android-specific check for element obscuration
*/
static async checkElementNotObscured(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
): Promise {
try {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
const attributes = await el.getAttributes();
// Check if element has proper frame/bounds
@@ -138,7 +143,7 @@ export default class Utilities {
* Check if element is stable (non-retry version)
*/
static async checkElementStable(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: StabilityOptions = {},
): Promise {
const { timeout = 2000, interval = 200, stableCount = 3 } = options;
@@ -165,7 +170,7 @@ export default class Utilities {
};
while (Date.now() - start < timeout) {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
const position = await getPosition(el);
if (!position) {
@@ -199,24 +204,21 @@ export default class Utilities {
* Waits for an element to become stable (not moving) by checking its position multiple times.
*/
static async waitForElementToStopMoving(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: StabilityOptions = {},
): Promise {
const { timeout = 5000 } = options;
- return this.executeWithRetry(
- () => this.checkElementStable(detoxElement, options),
- {
- timeout,
- description: 'Element stability',
- },
- );
+ return this.executeWithRetry(() => this.checkElementStable(elem, options), {
+ timeout,
+ description: 'Element stability',
+ });
}
/**
* Check element ready state (non-retry version)
*/
static async checkElementReadyState(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: {
timeout?: number;
checkStability?: boolean;
@@ -231,7 +233,7 @@ export default class Utilities {
checkEnabled = true,
} = options;
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
/**
* IMPORTANT: Default timeout behavior
*
@@ -279,7 +281,7 @@ export default class Utilities {
* Wait for element to be in a ready state (visible, enabled, stable)
*/
static async waitForReadyState(
- detoxElement: DetoxElement,
+ elem: EncapsulatedElementType,
options: {
timeout?: number;
checkStability?: boolean;
@@ -290,7 +292,7 @@ export default class Utilities {
const { timeout = TEST_CONFIG_DEFAULTS.timeout, elemDescription } = options;
return this.executeWithRetry(
- () => this.checkElementReadyState(detoxElement, options),
+ () => this.checkElementReadyState(elem, options),
{
timeout,
description: 'Element ready state check',
@@ -303,10 +305,10 @@ export default class Utilities {
* Wait for element to be visible and throw on failure
*/
static async waitForElementToBeVisible(
- detoxElement: DetoxElement | DetoxMatcher,
+ elem: DetoxMatcher | EncapsulatedElementType,
timeout: number = 2000,
): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
const isWebElement = this.isWebElement(el);
if (isWebElement) {
@@ -323,10 +325,10 @@ export default class Utilities {
* Wait for element to be not visible and throw on failure
*/
static async waitForElementToDisappear(
- detoxElement: DetoxElement | DetoxMatcher,
+ elem: DetoxMatcher | EncapsulatedElementType,
timeout: number = 2000,
): Promise {
- const el = (await detoxElement) as Detox.IndexableNativeElement;
+ const el = (await elem) as Detox.IndexableNativeElement;
const isWebElement = this.isWebElement(el);
if (isWebElement) {
// eslint-disable-next-line jest/valid-expect, @typescript-eslint/no-explicit-any
@@ -343,11 +345,11 @@ export default class Utilities {
* Returns true if element is visible, false if not visible or doesn't exist
*/
static async isElementVisible(
- detoxElement: DetoxElement | DetoxMatcher,
+ elem: DetoxMatcher | EncapsulatedElementType,
timeout: number = 2000,
): Promise {
try {
- await this.waitForElementToBeVisible(detoxElement, timeout);
+ await this.waitForElementToBeVisible(elem, timeout);
return true;
} catch {
return false;
diff --git a/tests/page-objects/AccountMenu/AccountMenu.ts b/tests/page-objects/AccountMenu/AccountMenu.ts
index 8d98dd9d389..f58989d915e 100644
--- a/tests/page-objects/AccountMenu/AccountMenu.ts
+++ b/tests/page-objects/AccountMenu/AccountMenu.ts
@@ -1,47 +1,48 @@
import { AccountsMenuSelectorsIDs } from '../../../app/components/Views/AccountsMenu/AccountsMenu.testIds';
import Matchers from '../../../tests/framework/Matchers';
import Gestures from '../../../tests/framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class AccountMenu {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountsMenuSelectorsIDs.ACCOUNTS_MENU_SCROLL_ID,
);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.BACK_BUTTON);
}
- get settingsButton(): DetoxElement {
+ get settingsButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.SETTINGS);
}
- get contactsButton(): DetoxElement {
+ get contactsButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.CONTACTS);
}
- get manageCardButton(): DetoxElement {
+ get manageCardButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.MANAGE_CARD);
}
- get permissionsButton(): DetoxElement {
+ get permissionsButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.PERMISSIONS);
}
- get aboutMetaMaskButton(): DetoxElement {
+ get aboutMetaMaskButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.ABOUT_METAMASK);
}
- get requestFeatureButton(): DetoxElement {
+ get requestFeatureButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.REQUEST_FEATURE);
}
- get supportButton(): DetoxElement {
+ get supportButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.SUPPORT);
}
- get lockButton(): DetoxElement {
+ get lockButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountsMenuSelectorsIDs.LOCK);
}
@@ -93,7 +94,7 @@ class AccountMenu {
});
}
- get notificationsButton(): DetoxElement {
+ get notificationsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountsMenuSelectorsIDs.NOTIFICATIONS_BUTTON,
);
diff --git a/tests/page-objects/Browser/AddBookmarkView.ts b/tests/page-objects/Browser/AddBookmarkView.ts
index e0aafe4e7e0..43356676495 100644
--- a/tests/page-objects/Browser/AddBookmarkView.ts
+++ b/tests/page-objects/Browser/AddBookmarkView.ts
@@ -1,13 +1,14 @@
import { AddBookmarkViewSelectorsIDs } from '../../../app/components/Views/AddBookmark/AddBookmarkView.testIds';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class AddFavoritesView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(AddBookmarkViewSelectorsIDs.CONTAINER);
}
- get addBookmarkButton(): DetoxElement {
+ get addBookmarkButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(AddBookmarkViewSelectorsIDs.CONFIRM_BUTTON)
: Matchers.getElementByLabel(AddBookmarkViewSelectorsIDs.CONFIRM_BUTTON);
diff --git a/tests/page-objects/Browser/BrowserView.ts b/tests/page-objects/Browser/BrowserView.ts
index 0946ce6178b..cc45482e9e5 100644
--- a/tests/page-objects/Browser/BrowserView.ts
+++ b/tests/page-objects/Browser/BrowserView.ts
@@ -11,6 +11,7 @@ import {
getTestDappLocalUrl,
getDappUrl,
} from '../../framework/fixtures/FixtureUtils';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import { DEFAULT_TAB_ID } from '../../framework/Constants';
import { Assertions, Gestures, Matchers, Utilities } from '../../framework';
@@ -19,68 +20,68 @@ interface TransactionParams {
}
class Browser {
- get reloadButton(): DetoxElement {
+ get reloadButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.RELOAD_BUTTON);
}
- get bookmarkButton(): DetoxElement {
+ get bookmarkButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.BOOKMARK_BUTTON);
}
- get newTabButton(): DetoxElement {
+ get newTabButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.NEW_TAB_BUTTON);
}
- get closeBrowserButton(): DetoxElement {
+ get closeBrowserButton(): EncapsulatedElementType {
return Matchers.getElementByID(
BrowserViewSelectorsIDs.BROWSER_CLOSE_BUTTON,
);
}
// Legacy getters for backward compatibility with existing tests
- get homeButton(): DetoxElement {
+ get homeButton(): EncapsulatedElementType {
// Home button removed, but kept for backward compatibility
// Tests using this should be updated
return this.newTabButton;
}
- get browserScreenID(): DetoxElement {
+ get browserScreenID(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.BROWSER_SCREEN_ID);
}
- get androidBrowserWebViewID(): DetoxElement {
+ get androidBrowserWebViewID(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID);
}
- get addressBar(): DetoxElement {
+ get addressBar(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.URL_INPUT);
}
- get urlInputBoxID(): DetoxElement {
+ get urlInputBoxID(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserURLBarSelectorsIDs.URL_INPUT);
}
- get clearURLButton(): DetoxElement {
+ get clearURLButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserURLBarSelectorsIDs.URL_CLEAR_ICON);
}
- get cancelUrlInputButton(): DetoxElement {
+ get cancelUrlInputButton(): EncapsulatedElementType {
return Matchers.getElementByID(
BrowserURLBarSelectorsIDs.CANCEL_BUTTON_ON_BROWSER_ID,
);
}
- get backToSafetyButton(): DetoxElement {
+ get backToSafetyButton(): EncapsulatedElementType {
return Matchers.getElementByText(
BrowserViewSelectorsText.BACK_TO_SAFETY_BUTTON,
);
}
- get returnHomeButton(): DetoxElement {
+ get returnHomeButton(): EncapsulatedElementType {
return Matchers.getElementByText(BrowserViewSelectorsText.RETURN_HOME);
}
- get addFavouritesButton(): DetoxElement {
+ get addFavouritesButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.ADD_NEW_TAB);
}
@@ -103,33 +104,33 @@ class Browser {
);
}
- get multiTabButton(): DetoxElement {
+ get multiTabButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.ADD_NEW_TAB);
}
- get DefaultAvatarImageForLocalHost(): DetoxElement {
+ get DefaultAvatarImageForLocalHost(): EncapsulatedElementType {
return Matchers.getElementByLabel('L');
}
- get networkAvatarOrAccountButton(): DetoxElement {
+ get networkAvatarOrAccountButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountOverviewSelectorsIDs.ACCOUNT_BUTTON);
}
- get addBookmarkButton(): DetoxElement {
+ get addBookmarkButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(AddBookmarkViewSelectorsIDs.CONFIRM_BUTTON)
: Matchers.getElementByLabel(AddBookmarkViewSelectorsIDs.CONFIRM_BUTTON);
}
- get tabsNumber(): DetoxElement {
+ get tabsNumber(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.TABS_NUMBER);
}
- get closeAllTabsButton(): DetoxElement {
+ get closeAllTabsButton(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.CLOSE_ALL_TABS);
}
- get noTabsMessage(): DetoxElement {
+ get noTabsMessage(): EncapsulatedElementType {
return Matchers.getElementByID(BrowserViewSelectorsIDs.NO_TABS_MESSAGE);
}
diff --git a/tests/page-objects/Browser/Confirmations/AlertSystem.ts b/tests/page-objects/Browser/Confirmations/AlertSystem.ts
index 1591bbf37e8..fca2d1634a8 100644
--- a/tests/page-objects/Browser/Confirmations/AlertSystem.ts
+++ b/tests/page-objects/Browser/Confirmations/AlertSystem.ts
@@ -8,59 +8,60 @@ import {
AlertTypeIDs,
} from '../../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class AlertSystem {
- get securityAlertBanner(): DetoxElement {
+ get securityAlertBanner(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationTopSheetSelectorsIDs.SECURITY_ALERT_BANNER_REDESIGNED,
);
}
- get securityAlertResponseFailedBanner(): DetoxElement {
+ get securityAlertResponseFailedBanner(): EncapsulatedElementType {
return Matchers.getElementByText(
ConfirmationTopSheetSelectorsText.BANNER_FAILED_TITLE,
);
}
- get securityAlertResponseMaliciousBanner(): DetoxElement {
+ get securityAlertResponseMaliciousBanner(): EncapsulatedElementType {
return Matchers.getElementByText(
ConfirmationTopSheetSelectorsText.BANNER_MALICIOUS_TITLE,
);
}
- get inlineAlert(): DetoxElement {
+ get inlineAlert(): EncapsulatedElementType {
return Matchers.getElementByID(AlertTypeIDs.INLINE_ALERT);
}
- get alertMismatchTitle(): DetoxElement {
+ get alertMismatchTitle(): EncapsulatedElementType {
return Matchers.getElementByText(
AlertModalSelectorsText.ALERT_ORIGIN_MISMATCH_TITLE,
);
}
- get acknowledgeAlertModal(): DetoxElement {
+ get acknowledgeAlertModal(): EncapsulatedElementType {
return Matchers.getElementByID(AlertModalSelectorsIDs.ALERT_MODAL_CHECKBOX);
}
- get acknowledgeAlertModalButton(): DetoxElement {
+ get acknowledgeAlertModalButton(): EncapsulatedElementType {
return Matchers.getElementByID(
AlertModalSelectorsIDs.ALERT_MODAL_ACKNOWLEDGE_BUTTON,
);
}
- get confirmAlertModal(): DetoxElement {
+ get confirmAlertModal(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmAlertModalSelectorsIDs.CONFIRM_ALERT_MODAL,
);
}
- get confirmAlertModalButton(): DetoxElement {
+ get confirmAlertModalButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmAlertModalSelectorsIDs.CONFIRM_ALERT_BUTTON,
);
}
- get acknowledgeConfirmAlertModal(): DetoxElement {
+ get acknowledgeConfirmAlertModal(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmAlertModalSelectorsIDs.CONFIRM_ALERT_CHECKBOX,
);
diff --git a/tests/page-objects/Browser/Confirmations/ConfirmationUITypes.ts b/tests/page-objects/Browser/Confirmations/ConfirmationUITypes.ts
index 3af8723b5df..e3710fe225d 100644
--- a/tests/page-objects/Browser/Confirmations/ConfirmationUITypes.ts
+++ b/tests/page-objects/Browser/Confirmations/ConfirmationUITypes.ts
@@ -1,12 +1,13 @@
import { ConfirmationUIType } from '../../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Matchers from '../../../framework/Matchers';
+import { EncapsulatedElementType } from '../../../framework';
class ConfirmationUITypes {
- get ModalConfirmationContainer(): DetoxElement {
+ get ModalConfirmationContainer(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationUIType.MODAL);
}
- get FlatConfirmationContainer(): DetoxElement {
+ get FlatConfirmationContainer(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationUIType.FLAT);
}
}
diff --git a/tests/page-objects/Browser/Confirmations/FooterActions.ts b/tests/page-objects/Browser/Confirmations/FooterActions.ts
index 906abbbed2f..b2dcfeface9 100644
--- a/tests/page-objects/Browser/Confirmations/FooterActions.ts
+++ b/tests/page-objects/Browser/Confirmations/FooterActions.ts
@@ -3,17 +3,18 @@ import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import TestHelpers from '../../../helpers';
import { encapsulatedAction } from '../../../framework/encapsulatedAction';
+import { EncapsulatedElementType } from '../../../framework/EncapsulatedElement';
import PlaywrightMatchers from '../../../framework/PlaywrightMatchers';
import PlaywrightGestures from '../../../framework/PlaywrightGestures';
class FooterActions {
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationFooterSelectorIDs.CONFIRM_BUTTON,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationFooterSelectorIDs.CANCEL_BUTTON);
}
diff --git a/tests/page-objects/Browser/Confirmations/RequestTypes.ts b/tests/page-objects/Browser/Confirmations/RequestTypes.ts
index 88b2fdd2aa6..506ac6d2bd7 100644
--- a/tests/page-objects/Browser/Confirmations/RequestTypes.ts
+++ b/tests/page-objects/Browser/Confirmations/RequestTypes.ts
@@ -1,14 +1,15 @@
import { ConfirmationRequestTypeIDs } from '../../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Matchers from '../../../framework/Matchers';
+import { EncapsulatedElementType } from '../../../framework';
class RequestTypes {
- get PersonalSignRequest(): DetoxElement {
+ get PersonalSignRequest(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRequestTypeIDs.PERSONAL_SIGN_REQUEST,
);
}
- get TypedSignRequest(): DetoxElement {
+ get TypedSignRequest(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRequestTypeIDs.TYPED_SIGN_REQUEST,
);
diff --git a/tests/page-objects/Browser/Confirmations/RowComponents.ts b/tests/page-objects/Browser/Confirmations/RowComponents.ts
index 72984d7b29b..b20bf69ce7a 100644
--- a/tests/page-objects/Browser/Confirmations/RowComponents.ts
+++ b/tests/page-objects/Browser/Confirmations/RowComponents.ts
@@ -3,79 +3,80 @@ import {
GasFeeTokenSelectorIDs,
} from '../../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Matchers from '../../../framework/Matchers';
+import { EncapsulatedElementType } from '../../../framework';
class RowComponents {
- get AccountNetwork(): DetoxElement {
+ get AccountNetwork(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.ACCOUNT_NETWORK);
}
- get AdvancedDetails(): DetoxElement {
+ get AdvancedDetails(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.ADVANCED_DETAILS,
);
}
- get FromTo(): DetoxElement {
+ get FromTo(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.FROM_TO);
}
- get GasFeesDetails(): DetoxElement {
+ get GasFeesDetails(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.GAS_FEES_DETAILS,
);
}
- get Message(): DetoxElement {
+ get Message(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.MESSAGE);
}
- get OriginInfo(): DetoxElement {
+ get OriginInfo(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.ORIGIN_INFO);
}
- get SimulationDetails(): DetoxElement {
+ get SimulationDetails(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.SIMULATION_DETAILS,
);
}
- get SiweSigningAccountInfo(): DetoxElement {
+ get SiweSigningAccountInfo(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.SIWE_SIGNING_ACCOUNT_INFO,
);
}
- get TokenHero(): DetoxElement {
+ get TokenHero(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.TOKEN_HERO);
}
- get ApproveRow(): DetoxElement {
+ get ApproveRow(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.APPROVE_ROW);
}
- get NetworkAndOrigin(): DetoxElement {
+ get NetworkAndOrigin(): EncapsulatedElementType {
return Matchers.getElementByID(ConfirmationRowComponentIDs.NETWORK);
}
- get NetworkFeePaidByMetaMask(): DetoxElement {
+ get NetworkFeePaidByMetaMask(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.PAID_BY_METAMASK,
);
}
- get NetworkFeeGasFeeTokenPill(): DetoxElement {
+ get NetworkFeeGasFeeTokenPill(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationRowComponentIDs.GAS_FEE_TOKEN_PILL,
);
}
- get NetworkFeeGasFeeTokenSymbol(): DetoxElement {
+ get NetworkFeeGasFeeTokenSymbol(): EncapsulatedElementType {
return Matchers.getElementByID(
GasFeeTokenSelectorIDs.SELECTED_GAS_FEE_TOKEN_SYMBOL,
);
}
- get NetworkFeeGasFeeTokenArrow(): DetoxElement {
+ get NetworkFeeGasFeeTokenArrow(): EncapsulatedElementType {
return Matchers.getElementByID(
GasFeeTokenSelectorIDs.SELECTED_GAS_FEE_TOKEN_ARROW,
);
diff --git a/tests/page-objects/Browser/ConnectBottomSheet.ts b/tests/page-objects/Browser/ConnectBottomSheet.ts
index 512d3f08635..e5847b3697a 100644
--- a/tests/page-objects/Browser/ConnectBottomSheet.ts
+++ b/tests/page-objects/Browser/ConnectBottomSheet.ts
@@ -4,45 +4,46 @@ import {
} from '../../../app/components/Views/MultichainAccounts/shared/ConnectAccountBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import { CommonSelectorsIDs } from '../../../app/util/Common.testIds';
class ConnectBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectAccountBottomSheetSelectorsIDs.CONTAINER,
);
}
- get connectButton(): DetoxElement {
+ get connectButton(): EncapsulatedElementType {
return device.getPlatform() === 'android'
? Matchers.getElementByLabel(CommonSelectorsIDs.CONNECT_BUTTON)
: Matchers.getElementByID(CommonSelectorsIDs.CONNECT_BUTTON);
}
- get connectAccountsButton(): DetoxElement {
+ get connectAccountsButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectAccountBottomSheetSelectorsText.CONNECT_ACCOUNTS,
);
}
- get importButton(): DetoxElement {
+ get importButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectAccountBottomSheetSelectorsText.IMPORT_ACCOUNT,
);
}
- get selectAllButton(): DetoxElement {
+ get selectAllButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectAccountBottomSheetSelectorsText.SELECT_ALL,
);
}
- get selectMultiButton(): DetoxElement {
+ get selectMultiButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectAccountBottomSheetSelectorsIDs.SELECT_MULTI_BUTTON,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectAccountBottomSheetSelectorsIDs.CANCEL_BUTTON,
);
diff --git a/tests/page-objects/Browser/ConnectedAccountsModal.ts b/tests/page-objects/Browser/ConnectedAccountsModal.ts
index d1c82533ef2..c912d84b1f3 100644
--- a/tests/page-objects/Browser/ConnectedAccountsModal.ts
+++ b/tests/page-objects/Browser/ConnectedAccountsModal.ts
@@ -11,102 +11,103 @@ import type {
NativeElement,
IndexableSystemElement,
} from 'detox/detox';
+import { EncapsulatedElementType } from '../../framework';
type DetoxElement = Promise<
IndexableNativeElement | NativeElement | IndexableSystemElement
>;
class ConnectedAccountsModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ConnectedAccountsSelectorsIDs.CONTAINER);
}
- get permissionsButton(): DetoxElement {
+ get permissionsButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectedAccountModalSelectorsText.PERMISSION_LINK,
);
}
- get networkPicker(): DetoxElement {
+ get networkPicker(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.NETWORK_PICKER,
);
}
- get disconnectAllButton(): DetoxElement {
+ get disconnectAllButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectedAccountModalSelectorsText.DISCONNECT_ALL,
);
}
- get disconnectButton(): DetoxElement {
+ get disconnectButton(): EncapsulatedElementType {
return Matchers.getElementByID(ConnectedAccountsSelectorsIDs.DISCONNECT);
}
- get disconnectAllAccountsAndNetworksButton(): DetoxElement {
+ get disconnectAllAccountsAndNetworksButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.DISCONNECT_ALL_ACCOUNTS_NETWORKS,
);
}
- get navigateToEditAccountsPermissionsButton(): DetoxElement {
+ get navigateToEditAccountsPermissionsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.NAVIGATE_TO_EDIT_ACCOUNTS_PERMISSIONS_BUTTON,
);
}
- get navigateToEditNetworksPermissionsButton(): DetoxElement {
+ get navigateToEditNetworksPermissionsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.NAVIGATE_TO_EDIT_NETWORKS_PERMISSIONS_BUTTON,
);
}
- get connectAccountsButton(): DetoxElement {
+ get connectAccountsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.CONNECT_ACCOUNTS_BUTTON,
);
}
- get managePermissionsButton(): DetoxElement {
+ get managePermissionsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.MANAGE_PERMISSIONS,
);
}
- get permissionsSummaryTab(): DetoxElement {
+ get permissionsSummaryTab(): EncapsulatedElementType {
return Matchers.getElementByText(
WalletViewSelectorsText.PERMISSIONS_SUMMARY_TAB,
);
}
- get accountsSummaryTab(): DetoxElement {
+ get accountsSummaryTab(): EncapsulatedElementType {
return Matchers.getElementByText(
WalletViewSelectorsText.ACCOUNTS_SUMMARY_TAB,
);
}
- get accountListBottomSheet(): DetoxElement {
+ get accountListBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.ACCOUNT_LIST_BOTTOM_SHEET,
);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(ConnectedAccountModalSelectorsText.TITLE);
}
- get selectAllNetworksButton(): DetoxElement {
+ get selectAllNetworksButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ConnectedAccountModalSelectorsText.SELECT_ALL,
);
}
- get disconnectNetworksButton(): DetoxElement {
+ get disconnectNetworksButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.DISCONNECT_NETWORKS_BUTTON,
);
}
- get confirmDisconnectNetworksButton(): DetoxElement {
+ get confirmDisconnectNetworksButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectedAccountsSelectorsIDs.CONFIRM_DISCONNECT_NETWORKS_BUTTON,
);
@@ -243,7 +244,9 @@ class ConnectedAccountsModal {
for (const accountName of possibleAccountNames) {
try {
- const textElement = await Matchers.getElementByText(accountName);
+ const textElement = (await Matchers.getElementByText(
+ accountName,
+ )) as Detox.NativeElement;
await waitFor(textElement).toBeVisible().withTimeout(1000);
displayedAccounts.push(accountName);
} catch (e) {
diff --git a/tests/page-objects/Browser/ContractApprovalBottomSheet.ts b/tests/page-objects/Browser/ContractApprovalBottomSheet.ts
index 9c746366cc9..eba72e84d4e 100644
--- a/tests/page-objects/Browser/ContractApprovalBottomSheet.ts
+++ b/tests/page-objects/Browser/ContractApprovalBottomSheet.ts
@@ -9,6 +9,7 @@
// } from '../../../app/components/Views/confirmations/legacy/components/ContractApprovalBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
// Temporary placeholders to prevent TypeScript errors
const ContractApprovalBottomSheetSelectorsIDs = {
@@ -27,55 +28,55 @@ const ContractApprovalBottomSheetSelectorsText = {
};
class ContractApprovalBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ContractApprovalBottomSheetSelectorsIDs.CONTAINER,
);
}
- get addNickName(): DetoxElement {
+ get addNickName(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.ADD_NICKNAME,
);
}
- get editNickName(): DetoxElement {
+ get editNickName(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.EDIT_NICKNAME,
);
}
- get rejectButton(): DetoxElement {
+ get rejectButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.REJECT,
);
}
- get approveButton(): DetoxElement {
+ get approveButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.APPROVE,
);
}
- get contractAddress(): DetoxElement {
+ get contractAddress(): EncapsulatedElementType {
return Matchers.getElementByID(
ContractApprovalBottomSheetSelectorsIDs.CONTRACT_ADDRESS,
);
}
- get nextButton(): DetoxElement {
+ get nextButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.NEXT,
);
}
- get approveTokenAmount(): DetoxElement {
+ get approveTokenAmount(): EncapsulatedElementType {
return Matchers.getElementByID(
ContractApprovalBottomSheetSelectorsIDs.APPROVE_TOKEN_AMOUNT,
);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ContractApprovalBottomSheetSelectorsText.CONFIRM,
);
diff --git a/tests/page-objects/Browser/DownloadFile.ts b/tests/page-objects/Browser/DownloadFile.ts
index 4b0965c1f9a..0e616b48c7e 100644
--- a/tests/page-objects/Browser/DownloadFile.ts
+++ b/tests/page-objects/Browser/DownloadFile.ts
@@ -9,17 +9,21 @@ class DownloadFile {
device.getPlatform() === 'android'
? Matchers.getElementByText('Download')
: Matchers.getElementByLabel('Download');
- await (await downloadButtonInDialog).tap();
+ await ((await downloadButtonInDialog) as Detox.NativeElement).tap();
}
async verifySuccessStateVisible(): Promise {
if (device.getPlatform() === 'ios') {
// Verify for iOS that system file saving dialog is visible
- waitFor(await Matchers.getElementByLabel('Save')).toExist();
+ waitFor(
+ (await Matchers.getElementByLabel('Save')) as Detox.NativeElement,
+ ).toExist();
} else {
// Verify for Android that toast after successful downloading is visible
waitFor(
- await Matchers.getElementByText('Downloaded successfully'),
+ (await Matchers.getElementByText(
+ 'Downloaded successfully',
+ )) as Detox.NativeElement,
).toExist();
}
}
diff --git a/tests/page-objects/Browser/ExternalWebsites/Security/CameraWebsite.ts b/tests/page-objects/Browser/ExternalWebsites/Security/CameraWebsite.ts
index 016ede9b28e..6969ee59aaa 100644
--- a/tests/page-objects/Browser/ExternalWebsites/Security/CameraWebsite.ts
+++ b/tests/page-objects/Browser/ExternalWebsites/Security/CameraWebsite.ts
@@ -7,9 +7,9 @@ class CameraWebsite {
async verifyRequestPermissionDialogVisible(): Promise {
if (device.getPlatform() === 'ios') {
await waitFor(
- await Matchers.getElementByLabel(
+ (await Matchers.getElementByLabel(
'Allow "localhost" to use your camera?',
- ),
+ )) as Detox.NativeElement,
).toExist();
// The WKWebView permission prompt is part of the app view hierarchy,
// not a system alert. Multiple elements may match "Allow", so pick
diff --git a/tests/page-objects/Browser/NetworkConnectMultiSelector.ts b/tests/page-objects/Browser/NetworkConnectMultiSelector.ts
index 4e3f12d17d0..00294776609 100644
--- a/tests/page-objects/Browser/NetworkConnectMultiSelector.ts
+++ b/tests/page-objects/Browser/NetworkConnectMultiSelector.ts
@@ -1,22 +1,22 @@
import { NetworkConnectMultiSelectorSelectorsIDs } from '../../../app/components/Views/NetworkConnect/NetworkConnectMultiSelector.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
-import { Assertions } from '../../framework';
+import { Assertions, EncapsulatedElementType } from '../../framework';
class NetworkConnectMultiSelector {
- get updateButton(): DetoxElement {
+ get updateButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkConnectMultiSelectorSelectorsIDs.UPDATE_CHAIN_PERMISSIONS,
);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkConnectMultiSelectorSelectorsIDs.BACK_BUTTON,
);
}
- getMultiselectElement(label: string): DetoxElement {
+ getMultiselectElement(label: string): EncapsulatedElementType {
return Matchers.getElementByLabel(label);
}
@@ -34,7 +34,7 @@ class NetworkConnectMultiSelector {
async isNetworkChainPermissionSelected(chainName: string): Promise {
const chainPermissionTestId = `${chainName}-selected`;
- const el = await Matchers.getElementByID(chainPermissionTestId);
+ const el = Matchers.getElementByID(chainPermissionTestId);
await Assertions.expectElementToBeVisible(el, {
timeout: 10000,
description: `Network chain permission ${chainName} should be selected`,
@@ -43,7 +43,7 @@ class NetworkConnectMultiSelector {
async isNetworkChainPermissionNotSelected(chainName: string): Promise {
const chainPermissionTestId = `${chainName}-not-selected`;
- const el = await Matchers.getElementByID(chainPermissionTestId);
+ const el = Matchers.getElementByID(chainPermissionTestId);
await Assertions.expectElementToBeVisible(el, {
timeout: 10000,
description: `Network chain permission ${chainName} should be selected`,
diff --git a/tests/page-objects/Browser/PermissionSummaryBottomSheet.ts b/tests/page-objects/Browser/PermissionSummaryBottomSheet.ts
index 684739ea828..ad2446ab941 100644
--- a/tests/page-objects/Browser/PermissionSummaryBottomSheet.ts
+++ b/tests/page-objects/Browser/PermissionSummaryBottomSheet.ts
@@ -4,38 +4,39 @@ import {
} from '../../../app/components/Views/MultichainAccounts/shared/PermissionSummaryBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class PermissionSummaryBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
PermissionSummaryBottomSheetSelectorsIDs.CONTAINER,
);
}
- get addNetworkPermissionContainer(): DetoxElement {
+ get addNetworkPermissionContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
PermissionSummaryBottomSheetSelectorsIDs.NETWORK_PERMISSIONS_CONTAINER,
);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PermissionSummaryBottomSheetSelectorsIDs.BACK_BUTTON,
);
}
- get connectedAccountsText(): DetoxElement {
+ get connectedAccountsText(): EncapsulatedElementType {
return Matchers.getElementByText(
PermissionSummaryBottomSheetSelectorsText.CONNECTED_ACCOUNTS_TEXT,
);
}
- get ethereumMainnetText(): DetoxElement {
+ get ethereumMainnetText(): EncapsulatedElementType {
return Matchers.getElementByText(
PermissionSummaryBottomSheetSelectorsText.ETHEREUM_MAINNET_LABEL,
);
}
- get accountPermissionLabelContainer(): DetoxElement {
+ get accountPermissionLabelContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
PermissionSummaryBottomSheetSelectorsIDs.ACCOUNT_PERMISSION_CONTAINER,
);
diff --git a/tests/page-objects/Browser/SigningBottomSheet.ts b/tests/page-objects/Browser/SigningBottomSheet.ts
index f46d62c5ba4..111f96b91fa 100644
--- a/tests/page-objects/Browser/SigningBottomSheet.ts
+++ b/tests/page-objects/Browser/SigningBottomSheet.ts
@@ -1,25 +1,26 @@
import { SigningBottomSheetSelectorsIDs } from '../../../app/components/Views/confirmations/legacy/components/SigningBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class SigningBottomSheet {
- get signButton(): DetoxElement {
+ get signButton(): EncapsulatedElementType {
return Matchers.getElementByID(SigningBottomSheetSelectorsIDs.SIGN_BUTTON);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SigningBottomSheetSelectorsIDs.CANCEL_BUTTON,
);
}
- get personalRequest(): DetoxElement {
+ get personalRequest(): EncapsulatedElementType {
return Matchers.getElementByID(
SigningBottomSheetSelectorsIDs.PERSONAL_REQUEST,
);
}
- get typedRequest(): DetoxElement {
+ get typedRequest(): EncapsulatedElementType {
return Matchers.getElementByID(
SigningBottomSheetSelectorsIDs.TYPED_REQUEST,
);
diff --git a/tests/page-objects/Browser/SpamFilterModal.ts b/tests/page-objects/Browser/SpamFilterModal.ts
index 2e64d80c64f..ee350d25e9a 100644
--- a/tests/page-objects/Browser/SpamFilterModal.ts
+++ b/tests/page-objects/Browser/SpamFilterModal.ts
@@ -1,13 +1,14 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { SpamFilterModalSelectorText } from '../../selectors/Browser/SpamFilterModal.selectors';
+import { EncapsulatedElementType } from '../../framework';
class SpamFilterModal {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(SpamFilterModalSelectorText.TITLE);
}
- get cancelButtonText(): DetoxElement {
+ get cancelButtonText(): EncapsulatedElementType {
return Matchers.getElementByText(SpamFilterModalSelectorText.CANCEL_BUTTON);
}
diff --git a/tests/page-objects/Browser/TestDApp.ts b/tests/page-objects/Browser/TestDApp.ts
index af94f18a47a..998f9914cd3 100644
--- a/tests/page-objects/Browser/TestDApp.ts
+++ b/tests/page-objects/Browser/TestDApp.ts
@@ -3,6 +3,7 @@ import enContent from '../../../locales/languages/en.json';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import { getTestDappLocalUrl } from '../../framework/fixtures/FixtureUtils';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import { BrowserViewSelectorsIDs } from '../../../app/components/Views/BrowserTab/BrowserView.testIds';
import { TestDappSelectorsWebIDs } from '../../selectors/Browser/TestDapp.selectors';
import Browser from './BrowserView';
@@ -18,11 +19,11 @@ interface ContractNavigationParams {
}
class TestDApp {
- get confirmButtonText(): DetoxElement {
+ get confirmButtonText(): EncapsulatedElementType {
return Matchers.getElementByText(CONFIRM_BUTTON_TEXT);
}
- get approveButtonText(): DetoxElement {
+ get approveButtonText(): EncapsulatedElementType {
return Matchers.getElementByText(APPROVE_BUTTON_TEXT);
}
diff --git a/tests/page-objects/Browser/TestSnaps.ts b/tests/page-objects/Browser/TestSnaps.ts
index 2691123352d..bf6de80d02b 100644
--- a/tests/page-objects/Browser/TestSnaps.ts
+++ b/tests/page-objects/Browser/TestSnaps.ts
@@ -20,7 +20,7 @@ import { IndexableWebElement } from 'detox/detox';
import Utilities from '../../framework/Utilities';
import { ConfirmationFooterSelectorIDs } from '../../../app/components/Views/confirmations/ConfirmationView.testIds';
import { waitForTestSnapsToLoad } from '../../flows/browser.flow';
-import { RetryOptions } from '../../framework';
+import { RetryOptions, EncapsulatedElementType } from '../../framework';
import { Json } from '@metamask/utils';
import ToastModal from '../wallet/ToastModal';
import SolanaTestDApp from './SolanaTestDApp';
@@ -29,65 +29,65 @@ export const TEST_SNAPS_URL =
'https://metamask.github.io/snaps/test-snaps/3.4.2/';
class TestSnaps {
- get getConnectSnapButton(): DetoxElement {
+ get getConnectSnapButton(): EncapsulatedElementType {
return Matchers.getElementByID(SNAP_INSTALL_CONNECT);
}
- get getApproveSnapPermissionsRequestButton(): DetoxElement {
+ get getApproveSnapPermissionsRequestButton(): EncapsulatedElementType {
return Matchers.getElementByID(SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE);
}
- get getConnectSnapInstallOkButton(): DetoxElement {
+ get getConnectSnapInstallOkButton(): EncapsulatedElementType {
return Matchers.getElementByID(SNAP_INSTALL_OK);
}
- get getApproveSignRequestButton(): DetoxElement {
+ get getApproveSignRequestButton(): EncapsulatedElementType {
return Matchers.getElementByID(
TestSnapBottomSheetSelectorWebIDS.BOTTOMSHEET_FOOTER_BUTTON_ID,
);
}
- get confirmSignatureButton(): DetoxElement {
+ get confirmSignatureButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationFooterSelectorIDs.CONFIRM_BUTTON,
);
}
- get solanaConfirmButton(): DetoxElement {
+ get solanaConfirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
'confirm-sign-message-confirm-snap-footer-button',
);
}
- get footerButton(): DetoxElement {
+ get footerButton(): EncapsulatedElementType {
return Matchers.getElementByID(
TestSnapBottomSheetSelectorWebIDS.DEFAULT_FOOTER_BUTTON_ID,
);
}
- get checkboxElement(): DetoxElement {
+ get checkboxElement(): EncapsulatedElementType {
return Matchers.getElementByID('snap-ui-renderer__checkbox');
}
- get dateTimePickerTouchable(): DetoxElement {
+ get dateTimePickerTouchable(): EncapsulatedElementType {
return Matchers.getElementByID(
'snap-ui-renderer__date-time-picker--datetime-touchable',
);
}
- get datePickerTouchable(): DetoxElement {
+ get datePickerTouchable(): EncapsulatedElementType {
return Matchers.getElementByID(
'snap-ui-renderer__date-time-picker--date-touchable',
);
}
- get timePickerTouchable(): DetoxElement {
+ get timePickerTouchable(): EncapsulatedElementType {
return Matchers.getElementByID(
'snap-ui-renderer__date-time-picker--time-touchable',
);
}
- get dateTimePickerOkButton(): DetoxElement {
+ get dateTimePickerOkButton(): EncapsulatedElementType {
return Matchers.getElementByText('OK');
}
diff --git a/tests/page-objects/Card/CardHomeView.ts b/tests/page-objects/Card/CardHomeView.ts
index d3256792879..596674283f5 100644
--- a/tests/page-objects/Card/CardHomeView.ts
+++ b/tests/page-objects/Card/CardHomeView.ts
@@ -1,41 +1,42 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { CardHomeSelectors } from '../../../app/components/UI/Card/Views/CardHome/CardHome.testIds';
+import { EncapsulatedElementType } from '../../framework';
class CardHomeView {
- get tryAgainButton(): DetoxElement {
+ get tryAgainButton(): EncapsulatedElementType {
return Matchers.getElementByID(CardHomeSelectors.TRY_AGAIN_BUTTON);
}
- get privacyToggleButton(): DetoxElement {
+ get privacyToggleButton(): EncapsulatedElementType {
return Matchers.getElementByID(CardHomeSelectors.PRIVACY_TOGGLE_BUTTON);
}
- get addFundsButton(): DetoxElement {
+ get addFundsButton(): EncapsulatedElementType {
return Matchers.getElementByID(CardHomeSelectors.ADD_FUNDS_BUTTON);
}
- get addFundsBottomSheet(): DetoxElement {
+ get addFundsBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(CardHomeSelectors.ADD_FUNDS_BOTTOM_SHEET);
}
- get addFundsBottomSheetDepositOption(): DetoxElement {
+ get addFundsBottomSheetDepositOption(): EncapsulatedElementType {
return Matchers.getElementByID(
CardHomeSelectors.ADD_FUNDS_BOTTOM_SHEET_DEPOSIT_OPTION,
);
}
- get addFundsBottomSheetSwapOption(): DetoxElement {
+ get addFundsBottomSheetSwapOption(): EncapsulatedElementType {
return Matchers.getElementByID(
CardHomeSelectors.ADD_FUNDS_BOTTOM_SHEET_SWAP_OPTION,
);
}
- get cardViewTitle(): DetoxElement {
+ get cardViewTitle(): EncapsulatedElementType {
return Matchers.getElementByID(CardHomeSelectors.CARD_VIEW_TITLE);
}
- get swapScreenSourceTokenArea(): DetoxElement {
+ get swapScreenSourceTokenArea(): EncapsulatedElementType {
return Matchers.getElementByID('source-token-area');
}
diff --git a/tests/page-objects/Common/ForgotPasswordModalView.ts b/tests/page-objects/Common/ForgotPasswordModalView.ts
index 8d581a98266..160ca002f25 100644
--- a/tests/page-objects/Common/ForgotPasswordModalView.ts
+++ b/tests/page-objects/Common/ForgotPasswordModalView.ts
@@ -5,75 +5,76 @@ import {
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { OnboardingSelectorText } from '../../../app/components/Views/Onboarding/Onboarding.testIds';
+import { EncapsulatedElementType } from '../../framework';
class ForgotPasswordModalView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ForgotPasswordModalSelectorsIDs.CONTAINER);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByID(ForgotPasswordModalSelectorsIDs.TITLE);
}
- get description(): DetoxElement {
+ get description(): EncapsulatedElementType {
return Matchers.getElementByID(ForgotPasswordModalSelectorsIDs.DESCRIPTION);
}
- get resetWalletButton(): DetoxElement {
+ get resetWalletButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ForgotPasswordModalSelectorsIDs.RESET_WALLET_BUTTON,
);
}
- get yesResetWalletButton(): DetoxElement {
+ get yesResetWalletButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ForgotPasswordModalSelectorsIDs.YES_RESET_WALLET_BUTTON,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ForgotPasswordModalSelectorsIDs.CANCEL_BUTTON,
);
}
- get warningText(): DetoxElement {
+ get warningText(): EncapsulatedElementType {
return Matchers.getElementByID(
ForgotPasswordModalSelectorsIDs.WARNING_TEXT,
);
}
- get titleText(): DetoxElement {
+ get titleText(): EncapsulatedElementType {
return Matchers.getElementByText(ForgotPasswordModalSelectorsText.TITLE);
}
- get descriptionText(): DetoxElement {
+ get descriptionText(): EncapsulatedElementType {
return Matchers.getElementByText(
ForgotPasswordModalSelectorsText.DESCRIPTION,
);
}
- get resetWalletText(): DetoxElement {
+ get resetWalletText(): EncapsulatedElementType {
return Matchers.getElementByText(
ForgotPasswordModalSelectorsText.RESET_WALLET,
);
}
- get yesResetWalletText(): DetoxElement {
+ get yesResetWalletText(): EncapsulatedElementType {
return Matchers.getElementByText(
ForgotPasswordModalSelectorsText.YES_RESET_WALLET,
);
}
- get cancelText(): DetoxElement {
+ get cancelText(): EncapsulatedElementType {
return Matchers.getElementByText(ForgotPasswordModalSelectorsText.CANCEL);
}
- get warningTextContent(): DetoxElement {
+ get warningTextContent(): EncapsulatedElementType {
return Matchers.getElementByText(ForgotPasswordModalSelectorsText.WARNING);
}
- get successBottomNotification(): DetoxElement {
+ get successBottomNotification(): EncapsulatedElementType {
return Matchers.getElementByText(
OnboardingSelectorText.SUCCESSFUL_WALLET_RESET,
);
diff --git a/tests/page-objects/CommonView.ts b/tests/page-objects/CommonView.ts
index b02157c1c5e..b98febbd85b 100644
--- a/tests/page-objects/CommonView.ts
+++ b/tests/page-objects/CommonView.ts
@@ -4,21 +4,22 @@ import {
CommonSelectorsIDs,
CommonSelectorsText,
} from '../../app/util/Common.testIds';
+import { EncapsulatedElementType } from '../framework';
class CommonView {
- get okAlertByText(): DetoxElement {
+ get okAlertByText(): EncapsulatedElementType {
return Matchers.getElementByText('OK');
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.BACK_ARROW_BUTTON);
}
- get errorMessage(): DetoxElement {
+ get errorMessage(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.ERROR_MESSAGE);
}
- get okAlertButton(): DetoxElement {
+ get okAlertButton(): EncapsulatedElementType {
return Matchers.getElementByText(CommonSelectorsText.OK_ALERT_BUTTON);
}
diff --git a/tests/page-objects/Confirmation/ConfirmationView.ts b/tests/page-objects/Confirmation/ConfirmationView.ts
index 2cbdb81f046..466a650f499 100644
--- a/tests/page-objects/Confirmation/ConfirmationView.ts
+++ b/tests/page-objects/Confirmation/ConfirmationView.ts
@@ -1,14 +1,15 @@
import { ConfirmationTopSheetSelectorsIDs } from '../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class ConfirmationView {
- get securityAlertBanner(): DetoxElement {
+ get securityAlertBanner(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationTopSheetSelectorsIDs.SECURITY_ALERT_BANNER,
);
}
- get securityAlertResponseFailedBanner(): DetoxElement {
+ get securityAlertResponseFailedBanner(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationTopSheetSelectorsIDs.SECURITY_ALERT_RESPONSE_FAILED_BANNER,
);
diff --git a/tests/page-objects/Confirmation/GasFeeTokenModal.ts b/tests/page-objects/Confirmation/GasFeeTokenModal.ts
index f994d43fc70..a09a83267c9 100644
--- a/tests/page-objects/Confirmation/GasFeeTokenModal.ts
+++ b/tests/page-objects/Confirmation/GasFeeTokenModal.ts
@@ -1,9 +1,14 @@
-import { Assertions, Gestures, Matchers } from '../../framework';
+import {
+ Assertions,
+ Gestures,
+ Matchers,
+ EncapsulatedElementType,
+} from '../../framework';
import { GasFeeTokenModalSelectorsText } from '../../../app/components/Views/confirmations/ConfirmationView.testIds';
class GasFeeTokenModal {
- getTokenItem(symbol: string): DetoxElement {
+ getTokenItem(symbol: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${GasFeeTokenModalSelectorsText.GAS_FEE_TOKEN_ITEM}-${symbol}`,
);
@@ -16,27 +21,27 @@ class GasFeeTokenModal {
}
async checkAmountToken(symbol: string, amount: string): Promise {
- const amountElement = await Matchers.getElementByID(
+ const amountElement = (await Matchers.getElementByID(
`${GasFeeTokenModalSelectorsText.GAS_FEE_TOKEN_AMOUNT}-${symbol}`,
- );
+ )) as Detox.IndexableNativeElement;
const amountElementAttributes = await amountElement.getAttributes();
const amountElementLabel = this.elementSafe(amountElementAttributes);
await Assertions.checkIfTextMatches(amountElementLabel, amount);
}
async checkBalance(symbol: string, balance: string): Promise {
- const balanceElement = await Matchers.getElementByID(
+ const balanceElement = (await Matchers.getElementByID(
`${GasFeeTokenModalSelectorsText.GAS_FEE_TOKEN_BALANCE}-${symbol}`,
- );
+ )) as Detox.IndexableNativeElement;
const balanceElementAttributes = await balanceElement.getAttributes();
const balanceElementLabel = this.elementSafe(balanceElementAttributes);
await Assertions.checkIfTextMatches(balanceElementLabel, balance);
}
async checkAmountFiat(symbol: string, amountFiat: string): Promise {
- const amountFiatElement = await Matchers.getElementByID(
+ const amountFiatElement = (await Matchers.getElementByID(
`${GasFeeTokenModalSelectorsText.GAS_FEE_TOKEN_AMOUNT_FIAT}-${symbol}`,
- );
+ )) as Detox.IndexableNativeElement;
await Assertions.expectElementToBeVisible(amountFiatElement, {
description: `Amount fiat for ${symbol} is visible`,
diff --git a/tests/page-objects/Confirmation/TokenApproveConfirmation.ts b/tests/page-objects/Confirmation/TokenApproveConfirmation.ts
index fcddb923f4d..7e00fb517a2 100644
--- a/tests/page-objects/Confirmation/TokenApproveConfirmation.ts
+++ b/tests/page-objects/Confirmation/TokenApproveConfirmation.ts
@@ -1,24 +1,25 @@
import { ApproveComponentIDs } from '../../../app/components/Views/confirmations/ConfirmationView.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
// This components are used to check the approve confirmation specific components in the confirmation modal
class TokenApproveConfirmation {
- get SpendingCapValue(): DetoxElement {
+ get SpendingCapValue(): EncapsulatedElementType {
return Matchers.getElementByID(ApproveComponentIDs.SPENDING_CAP_VALUE);
}
- get EditSpendingCapButton(): DetoxElement {
+ get EditSpendingCapButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ApproveComponentIDs.EDIT_SPENDING_CAP_BUTTON,
);
}
- get EditSpendingCapInput(): DetoxElement {
+ get EditSpendingCapInput(): EncapsulatedElementType {
return Matchers.getElementByID(ApproveComponentIDs.EDIT_SPENDING_CAP_INPUT);
}
- get EditSpendingCapSaveButton(): DetoxElement {
+ get EditSpendingCapSaveButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ApproveComponentIDs.EDIT_SPENDING_CAP_SAVE_BUTTON,
);
diff --git a/tests/page-objects/Earn/EarnLendingView.ts b/tests/page-objects/Earn/EarnLendingView.ts
index 6c2c41ad717..538ed314904 100644
--- a/tests/page-objects/Earn/EarnLendingView.ts
+++ b/tests/page-objects/Earn/EarnLendingView.ts
@@ -6,61 +6,62 @@ import {
EarnLendingViewSelectorsIDs,
EarnLendingViewSelectorsText,
} from '../../selectors/Earn/EarnLendingView.selectors';
+import { EncapsulatedElementType } from '../../framework';
class EarnLendingView {
- get withdrawButton(): DetoxElement {
+ get withdrawButton(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.WITHDRAW_BUTTON);
}
- get depositButton(): DetoxElement {
+ get depositButton(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.DEPOSIT_BUTTON);
}
- get confirmationFooter(): DetoxElement {
+ get confirmationFooter(): EncapsulatedElementType {
return Matchers.getElementByID(
EarnLendingViewSelectorsIDs.CONFIRMATION_FOOTER,
);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.CONFIRM_BUTTON);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.CANCEL_BUTTON);
}
- get depositInfoSection(): DetoxElement {
+ get depositInfoSection(): EncapsulatedElementType {
return Matchers.getElementByID(
EarnLendingViewSelectorsIDs.DEPOSIT_INFO_SECTION,
);
}
- get depositReceiveSection(): DetoxElement {
+ get depositReceiveSection(): EncapsulatedElementType {
return Matchers.getElementByID(
EarnLendingViewSelectorsIDs.DEPOSIT_RECEIVE_SECTION,
);
}
- get progressBar(): DetoxElement {
+ get progressBar(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.PROGRESS_BAR);
}
- get supplyTitle(): DetoxElement {
+ get supplyTitle(): EncapsulatedElementType {
return Matchers.getElementByText(EarnLendingViewSelectorsText.SUPPLY);
}
- get reviewButton(): DetoxElement {
+ get reviewButton(): EncapsulatedElementType {
return Matchers.getElementByID(EarnLendingViewSelectorsIDs.REVIEW_BUTTON);
}
- get withdrawalTimeLabel(): DetoxElement {
+ get withdrawalTimeLabel(): EncapsulatedElementType {
return Matchers.getElementByText(
EarnLendingViewSelectorsText.WITHDRAWAL_TIME,
);
}
- get confirmButtonByLabel(): DetoxElement {
+ get confirmButtonByLabel(): EncapsulatedElementType {
return Matchers.getElementByText(EarnLendingViewSelectorsText.CONFIRM);
}
diff --git a/tests/page-objects/ErrorBoundaryView/ErrorBoundaryView.ts b/tests/page-objects/ErrorBoundaryView/ErrorBoundaryView.ts
index a2b320e03d3..6701b87e13c 100644
--- a/tests/page-objects/ErrorBoundaryView/ErrorBoundaryView.ts
+++ b/tests/page-objects/ErrorBoundaryView/ErrorBoundaryView.ts
@@ -1,13 +1,14 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { ErrorBoundarySelectorsText } from '../../selectors/ErrorBoundary/ErrorBoundaryView.selectors';
+import { EncapsulatedElementType } from '../../framework';
class ErrorBoundaryView {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(ErrorBoundarySelectorsText.TITLE);
}
- get srpLinkText(): DetoxElement {
+ get srpLinkText(): EncapsulatedElementType {
return Matchers.getElementByText(
ErrorBoundarySelectorsText.SAVE_YOUR_SRP_TEXT,
);
diff --git a/tests/page-objects/MultichainAccounts/AccountDetails.ts b/tests/page-objects/MultichainAccounts/AccountDetails.ts
index f650caf8763..aa14ac55508 100644
--- a/tests/page-objects/MultichainAccounts/AccountDetails.ts
+++ b/tests/page-objects/MultichainAccounts/AccountDetails.ts
@@ -2,53 +2,54 @@ import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { AccountDetailsIds } from '../../../app/components/Views/MultichainAccounts/AccountDetails.testIds';
import { ExportCredentialsIds } from '../../../app/components/Views/MultichainAccounts/AccountDetails/ExportCredentials.testIds';
+import { EncapsulatedElementType } from '../../framework';
class AccountDetails {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.ACCOUNT_DETAILS_CONTAINER);
}
- get shareAddress(): DetoxElement {
+ get shareAddress(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.ACCOUNT_ADDRESS_LINK);
}
- get editAccountName(): DetoxElement {
+ get editAccountName(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.ACCOUNT_NAME_LINK);
}
- get editWalletName(): DetoxElement {
+ get editWalletName(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.WALLET_NAME_LINK);
}
- get networksLink(): DetoxElement {
+ get networksLink(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.NETWORKS_LINK);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.BACK_BUTTON);
}
- get deleteAccountLink(): DetoxElement {
+ get deleteAccountLink(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.REMOVE_ACCOUNT_BUTTON);
}
- get accountSrpLink(): DetoxElement {
+ get accountSrpLink(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountDetailsIds.SECRET_RECOVERY_PHRASE_LINK,
);
}
- get exportPrivateKeyButton(): DetoxElement {
+ get exportPrivateKeyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ExportCredentialsIds.EXPORT_PRIVATE_KEY_BUTTON,
);
}
- get privateKeysLink(): DetoxElement {
+ get privateKeysLink(): EncapsulatedElementType {
return Matchers.getElementByID(AccountDetailsIds.PRIVATE_KEYS_LINK);
}
- get exportSrpButton(): DetoxElement {
+ get exportSrpButton(): EncapsulatedElementType {
return Matchers.getElementByID(ExportCredentialsIds.EXPORT_SRP_BUTTON);
}
diff --git a/tests/page-objects/MultichainAccounts/AddressList.ts b/tests/page-objects/MultichainAccounts/AddressList.ts
index 20d82481856..626f177fb73 100644
--- a/tests/page-objects/MultichainAccounts/AddressList.ts
+++ b/tests/page-objects/MultichainAccounts/AddressList.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { AddressListIds } from '../../../app/components/Views/MultichainAccounts/AddressList/AddressList.testIds';
+import { EncapsulatedElementType } from '../../framework';
class AddressList {
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(AddressListIds.GO_BACK);
}
diff --git a/tests/page-objects/MultichainAccounts/DeleteAccount.ts b/tests/page-objects/MultichainAccounts/DeleteAccount.ts
index 85cfdca07e2..f3b6f8a717d 100644
--- a/tests/page-objects/MultichainAccounts/DeleteAccount.ts
+++ b/tests/page-objects/MultichainAccounts/DeleteAccount.ts
@@ -1,15 +1,16 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { MultichainDeleteAccountSelectors } from '../../../app/components/Views/MultichainAccounts/sheets/DeleteAccount/DeleteAccount.testIds';
+import { EncapsulatedElementType } from '../../framework';
class DeleteAccount {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
MultichainDeleteAccountSelectors.DELETE_ACCOUNT_CONTAINER,
);
}
- get deleteAccountButton(): DetoxElement {
+ get deleteAccountButton(): EncapsulatedElementType {
return Matchers.getElementByID(
MultichainDeleteAccountSelectors.DELETE_ACCOUNT_REMOVE_BUTTON,
);
diff --git a/tests/page-objects/MultichainAccounts/EditAccountName.ts b/tests/page-objects/MultichainAccounts/EditAccountName.ts
index ecd0c8b11b7..ff93b249aa1 100644
--- a/tests/page-objects/MultichainAccounts/EditAccountName.ts
+++ b/tests/page-objects/MultichainAccounts/EditAccountName.ts
@@ -1,19 +1,20 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { EditAccountNameIds } from '../../../app/components/Views/MultichainAccounts/sheets/EditAccountName.testIds';
+import { EncapsulatedElementType } from '../../framework';
class EditAccountName {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
EditAccountNameIds.EDIT_ACCOUNT_NAME_CONTAINER,
);
}
- get accountNameInput(): DetoxElement {
+ get accountNameInput(): EncapsulatedElementType {
return Matchers.getElementByID(EditAccountNameIds.ACCOUNT_NAME_INPUT);
}
- get saveButton(): DetoxElement {
+ get saveButton(): EncapsulatedElementType {
return Matchers.getElementByID(EditAccountNameIds.SAVE_BUTTON);
}
diff --git a/tests/page-objects/MultichainAccounts/ExportCredentials.ts b/tests/page-objects/MultichainAccounts/ExportCredentials.ts
index fc17d7692cb..1a3c4df057f 100644
--- a/tests/page-objects/MultichainAccounts/ExportCredentials.ts
+++ b/tests/page-objects/MultichainAccounts/ExportCredentials.ts
@@ -2,39 +2,40 @@ import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { ExportCredentialsIds } from '../../../app/components/Views/MultichainAccounts/AccountDetails/ExportCredentials.testIds';
import { RevealSeedViewSelectorsIDs } from '../../../app/components/Views/RevealPrivateCredential/RevealSeedView.testIds';
+import { EncapsulatedElementType } from '../../framework';
class ExportCredentials {
- get srpInfoContainer(): DetoxElement {
+ get srpInfoContainer(): EncapsulatedElementType {
return Matchers.getElementByID(ExportCredentialsIds.CONTAINER);
}
- get revealContainer(): DetoxElement {
+ get revealContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_CONTAINER_ID,
);
}
- get exportPrivateKeyButton(): DetoxElement {
+ get exportPrivateKeyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ExportCredentialsIds.EXPORT_PRIVATE_KEY_BUTTON,
);
}
- get exportSrpButton(): DetoxElement {
+ get exportSrpButton(): EncapsulatedElementType {
return Matchers.getElementByID(ExportCredentialsIds.EXPORT_SRP_BUTTON);
}
- get passwordInput(): DetoxElement {
+ get passwordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.PASSWORD_INPUT_BOX_ID,
);
}
- get nextButton(): DetoxElement {
+ get nextButton(): EncapsulatedElementType {
return Matchers.getElementByID(ExportCredentialsIds.NEXT_BUTTON);
}
- get learnMoreButton(): DetoxElement {
+ get learnMoreButton(): EncapsulatedElementType {
return Matchers.getElementByID(ExportCredentialsIds.LEARN_MORE_BUTTON);
}
diff --git a/tests/page-objects/MultichainAccounts/PrivateKeyList.ts b/tests/page-objects/MultichainAccounts/PrivateKeyList.ts
index 8e9679cf465..94e5c0f1cca 100644
--- a/tests/page-objects/MultichainAccounts/PrivateKeyList.ts
+++ b/tests/page-objects/MultichainAccounts/PrivateKeyList.ts
@@ -4,27 +4,28 @@ import {
PrivateKeyListIds,
PrivateKeyListSelectorsText,
} from '../../../app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.testIds';
+import { EncapsulatedElementType } from '../../framework';
class PrivateKeyList {
- get passwordInput(): DetoxElement {
+ get passwordInput(): EncapsulatedElementType {
return Matchers.getElementByID(PrivateKeyListIds.PASSWORD_INPUT);
}
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByID(PrivateKeyListIds.CONTINUE_BUTTON);
}
- get copyToClipboard(): DetoxElement {
+ get copyToClipboard(): EncapsulatedElementType {
return Matchers.getElementByID(PrivateKeyListIds.COPY_TO_CLIPBOARD_BUTTON);
}
- get privateKeyCopiedLabel(): DetoxElement {
+ get privateKeyCopiedLabel(): EncapsulatedElementType {
return Matchers.getElementByText(
PrivateKeyListSelectorsText.PRIVATE_KEY_COPIED,
);
}
- get wrongPasswordLabel(): DetoxElement {
+ get wrongPasswordLabel(): EncapsulatedElementType {
return Matchers.getElementByText(
PrivateKeyListSelectorsText.WRONG_PASSWORD_ERROR,
);
diff --git a/tests/page-objects/MultichainAccounts/ShareAddress.ts b/tests/page-objects/MultichainAccounts/ShareAddress.ts
index c4eb3da3ff6..0364325e441 100644
--- a/tests/page-objects/MultichainAccounts/ShareAddress.ts
+++ b/tests/page-objects/MultichainAccounts/ShareAddress.ts
@@ -1,27 +1,28 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { ShareAddressIds } from '../../../app/components/Views/MultichainAccounts/sheets/ShareAddress/ShareAddress.testIds';
+import { EncapsulatedElementType } from '../../framework';
class ShareAddress {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ShareAddressIds.SHARE_ADDRESS_CONTAINER);
}
- get qrCode(): DetoxElement {
+ get qrCode(): EncapsulatedElementType {
return Matchers.getElementByID(ShareAddressIds.SHARE_ADDRESS_QR_CODE);
}
- get accountAddress(): DetoxElement {
+ get accountAddress(): EncapsulatedElementType {
return Matchers.getElementByID(
ShareAddressIds.SHARE_ADDRESS_ACCOUNT_ADDRESS,
);
}
- get copyButton(): DetoxElement {
+ get copyButton(): EncapsulatedElementType {
return Matchers.getElementByID(ShareAddressIds.SHARE_ADDRESS_COPY_BUTTON);
}
- get viewOnExplorerButton(): DetoxElement {
+ get viewOnExplorerButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ShareAddressIds.SHARE_ADDRESS_VIEW_ON_EXPLORER_BUTTON,
);
diff --git a/tests/page-objects/MultichainAccounts/SmartAccount.ts b/tests/page-objects/MultichainAccounts/SmartAccount.ts
index 4ec94510d22..5d5e8d9d9ca 100644
--- a/tests/page-objects/MultichainAccounts/SmartAccount.ts
+++ b/tests/page-objects/MultichainAccounts/SmartAccount.ts
@@ -1,13 +1,14 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { SmartAccountIds } from '../../../app/components/Views/MultichainAccounts/SmartAccount.testIds';
+import { EncapsulatedElementType } from '../../framework';
class SmartAccount {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(SmartAccountIds.SMART_ACCOUNT_CONTAINER);
}
- get smartAccountSwitch(): DetoxElement {
+ get smartAccountSwitch(): EncapsulatedElementType {
return Matchers.getElementByID(SmartAccountIds.SMART_ACCOUNT_SWITCH);
}
diff --git a/tests/page-objects/MultichainAccounts/WalletDetails.ts b/tests/page-objects/MultichainAccounts/WalletDetails.ts
index 07767b1621f..0be66fb387e 100644
--- a/tests/page-objects/MultichainAccounts/WalletDetails.ts
+++ b/tests/page-objects/MultichainAccounts/WalletDetails.ts
@@ -1,17 +1,18 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { WalletDetailsIds } from '../../../app/components/Views/MultichainAccounts/WalletDetails/WalletDetails.testIds';
+import { EncapsulatedElementType } from '../../framework';
class WalletDetails {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(WalletDetailsIds.WALLET_DETAILS_CONTAINER);
}
- get createAccountLink(): DetoxElement {
+ get createAccountLink(): EncapsulatedElementType {
return Matchers.getElementByID(WalletDetailsIds.ADD_ACCOUNT_BUTTON);
}
- get srpButton(): DetoxElement {
+ get srpButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletDetailsIds.REVEAL_SRP_BUTTON);
}
diff --git a/tests/page-objects/Network/NetworkAddedBottomSheet.ts b/tests/page-objects/Network/NetworkAddedBottomSheet.ts
index 31025a4d1cf..d4f0bb64310 100644
--- a/tests/page-objects/Network/NetworkAddedBottomSheet.ts
+++ b/tests/page-objects/Network/NetworkAddedBottomSheet.ts
@@ -4,21 +4,22 @@ import {
} from '../../../app/components/UI/NetworkModal/NetworkAddedBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class NetworkAddedBottomSheet {
- get switchNetwork(): DetoxElement {
+ get switchNetwork(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkAddedBottomSheetSelectorsText.SWITCH_NETWORK,
);
}
- get switchNetworkButton(): DetoxElement {
+ get switchNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkAddedBottomSheetSelectorsIDs.SWITCH_NETWORK_BUTTON,
);
}
- get closeNetworkButton(): DetoxElement {
+ get closeNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkAddedBottomSheetSelectorsIDs.CLOSE_NETWORK_BUTTON,
);
diff --git a/tests/page-objects/Network/NetworkApprovalBottomSheet.ts b/tests/page-objects/Network/NetworkApprovalBottomSheet.ts
index a5cbb427b3a..b567e4066fa 100644
--- a/tests/page-objects/Network/NetworkApprovalBottomSheet.ts
+++ b/tests/page-objects/Network/NetworkApprovalBottomSheet.ts
@@ -1,26 +1,27 @@
import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../app/components/UI/NetworkModal/NetworkApprovalBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class NetworkApprovalBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkApprovalBottomSheetSelectorsIDs.CONTAINER,
);
}
- get approvedButton(): DetoxElement {
+ get approvedButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkApprovalBottomSheetSelectorsIDs.APPROVE_BUTTON,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkApprovalBottomSheetSelectorsIDs.CANCEL_BUTTON,
);
}
- get displayName(): DetoxElement {
+ get displayName(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkApprovalBottomSheetSelectorsIDs.DISPLAY_NAME,
);
diff --git a/tests/page-objects/Network/NetworkEducationModal.ts b/tests/page-objects/Network/NetworkEducationModal.ts
index aa976501872..b7d585ada86 100644
--- a/tests/page-objects/Network/NetworkEducationModal.ts
+++ b/tests/page-objects/Network/NetworkEducationModal.ts
@@ -54,7 +54,7 @@ class NetworkEducationModal {
});
}
- get addToken(): DetoxElement {
+ get addToken(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkEducationModalSelectorsText.ADD_TOKEN,
);
diff --git a/tests/page-objects/Network/NetworkListModal.ts b/tests/page-objects/Network/NetworkListModal.ts
index 8115cecc9f3..56a5b897988 100644
--- a/tests/page-objects/Network/NetworkListModal.ts
+++ b/tests/page-objects/Network/NetworkListModal.ts
@@ -6,51 +6,52 @@ import { NetworksViewSelectorsIDs } from '../../../app/components/Views/Settings
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { NETWORK_MULTI_SELECTOR_TEST_IDS } from '../../../app/components/UI/NetworkMultiSelector/NetworkMultiSelector.constants';
+import { EncapsulatedElementType } from '../../framework';
class NetworkListModal {
- get networkScroll(): DetoxElement {
+ get networkScroll(): EncapsulatedElementType {
return Matchers.getElementByID(NetworkListModalSelectorsIDs.SCROLL);
}
- get closeIcon(): DetoxElement {
+ get closeIcon(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.CLOSE_ICON);
}
- get deleteNetworkButton(): DetoxElement {
+ get deleteNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkListModalSelectorsText.DELETE_NETWORK,
);
}
- get addPopularNetworkButton(): DetoxElement {
+ get addPopularNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkListModalSelectorsText.ADD_POPULAR_NETWORK_BUTTON,
);
}
- get networkSearchInput(): DetoxElement {
+ get networkSearchInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.SEARCH_NETWORK_INPUT_BOX_ID,
);
}
- get selectNetwork(): DetoxElement {
+ get selectNetwork(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkListModalSelectorsText.SELECT_NETWORK,
);
}
- get testNetToggle(): DetoxElement {
+ get testNetToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkListModalSelectorsIDs.TEST_NET_TOGGLE,
);
}
- get deleteButton(): DetoxElement {
+ get deleteButton(): EncapsulatedElementType {
return Matchers.getElementByID('delete-network-button');
}
- get popularNetworksContainer(): DetoxElement {
+ get popularNetworksContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
NETWORK_MULTI_SELECTOR_TEST_IDS.POPULAR_NETWORKS_CONTAINER,
);
@@ -59,7 +60,7 @@ class NetworkListModal {
async getCustomNetwork(
network: string,
custom = false,
- ): Promise {
+ ): Promise {
if (device.getPlatform() === 'android' || !custom) {
return Matchers.getElementByText(network);
}
diff --git a/tests/page-objects/Network/NetworkNonPemittedBottomSheet.ts b/tests/page-objects/Network/NetworkNonPemittedBottomSheet.ts
index 50e466a4003..201875b45cf 100644
--- a/tests/page-objects/Network/NetworkNonPemittedBottomSheet.ts
+++ b/tests/page-objects/Network/NetworkNonPemittedBottomSheet.ts
@@ -4,51 +4,52 @@ import {
NetworkNonPemittedBottomSheetSelectorsIDs,
NetworkNonPemittedBottomSheetSelectorsText,
} from '../../../app/components/Views/NetworkConnect/NetworkNonPemittedBottomSheet.testIds';
+import { EncapsulatedElementType } from '../../framework';
class NetworkNonPemittedBottomSheet {
- get addThisNetworkTitle(): DetoxElement {
+ get addThisNetworkTitle(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkNonPemittedBottomSheetSelectorsText.ADD_THIS_NETWORK_TITLE,
);
}
- get sepoliaNetworkName(): DetoxElement {
+ get sepoliaNetworkName(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkNonPemittedBottomSheetSelectorsText.SEPOLIA_NETWORK_NAME,
);
}
- get ethereumMainNetNetworkName(): DetoxElement {
+ get ethereumMainNetNetworkName(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkNonPemittedBottomSheetSelectorsText.ETHEREUM_MAIN_NET_NETWORK_NAME,
);
}
- get addThisNetworkButton(): DetoxElement {
+ get addThisNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkNonPemittedBottomSheetSelectorsIDs.ADD_THIS_NETWORK_BUTTON,
);
}
- get lineaSepoliaNetworkName(): DetoxElement {
+ get lineaSepoliaNetworkName(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkNonPemittedBottomSheetSelectorsText.LINEA_SEPOLIA_NETWORK_NAME,
);
}
- get elysiumTestnetNetworkName(): DetoxElement {
+ get elysiumTestnetNetworkName(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkNonPemittedBottomSheetSelectorsText.ELYSIUM_TESTNET_NETWORK_NAME,
);
}
- get chooseFromPermittedNetworksButton(): DetoxElement {
+ get chooseFromPermittedNetworksButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkNonPemittedBottomSheetSelectorsIDs.CHOOSE_FROM_PERMITTED_NETWORKS_BUTTON,
);
}
- get editPermissionsButton(): DetoxElement {
+ get editPermissionsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkNonPemittedBottomSheetSelectorsIDs.EDIT_PERMISSIONS_BUTTON,
);
diff --git a/tests/page-objects/Onboarding/CreatePasswordView.ts b/tests/page-objects/Onboarding/CreatePasswordView.ts
index 99c6445d88b..611c5fb642e 100644
--- a/tests/page-objects/Onboarding/CreatePasswordView.ts
+++ b/tests/page-objects/Onboarding/CreatePasswordView.ts
@@ -17,7 +17,7 @@ import { PlatformDetector } from '../../framework/PlatformLocator';
import { ImportFromSeedSelectorsIDs } from '../../../app/components/Views/ImportFromSecretRecoveryPhrase/ImportFromSeed.testIds';
class CreatePasswordView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ChoosePasswordSelectorsIDs.CONTAINER_ID);
}
@@ -140,7 +140,7 @@ class CreatePasswordView {
});
}
- get iUnderstandCheckboxNewWallet(): DetoxElement {
+ get iUnderstandCheckboxNewWallet(): EncapsulatedElementType {
return Matchers.getElementByID(
ChoosePasswordSelectorsIDs.I_UNDERSTAND_CHECKBOX_ID,
);
@@ -166,7 +166,7 @@ class CreatePasswordView {
});
}
- get passwordError(): DetoxElement {
+ get passwordError(): EncapsulatedElementType {
return Matchers.getElementByText(enContent.import_from_seed.password_error);
}
diff --git a/tests/page-objects/Onboarding/ExperienceEnhancerBottomSheet.ts b/tests/page-objects/Onboarding/ExperienceEnhancerBottomSheet.ts
index 28c6fe48906..3a7192a6d7a 100644
--- a/tests/page-objects/Onboarding/ExperienceEnhancerBottomSheet.ts
+++ b/tests/page-objects/Onboarding/ExperienceEnhancerBottomSheet.ts
@@ -13,7 +13,7 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import PlaywrightGestures from '../../framework/PlaywrightGestures';
class ExperienceEnhancerBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ExperienceEnhancerBottomSheetSelectorsIDs.BOTTOM_SHEET,
);
diff --git a/tests/page-objects/Onboarding/ImportWalletView.ts b/tests/page-objects/Onboarding/ImportWalletView.ts
index 26e106187fd..9fa77492328 100644
--- a/tests/page-objects/Onboarding/ImportWalletView.ts
+++ b/tests/page-objects/Onboarding/ImportWalletView.ts
@@ -17,7 +17,7 @@ import UnifiedGestures from '../../framework/UnifiedGestures';
import PlaywrightGestures from '../../framework/PlaywrightGestures';
class ImportWalletView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ImportFromSeedSelectorsIDs.CONTAINER_ID);
}
@@ -35,13 +35,13 @@ class ImportWalletView {
});
}
- get newPasswordInput(): DetoxElement {
+ get newPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
ChoosePasswordSelectorsIDs.NEW_PASSWORD_INPUT_ID,
);
}
- get confirmPasswordInput(): DetoxElement {
+ get confirmPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
ChoosePasswordSelectorsIDs.CONFIRM_PASSWORD_INPUT_ID,
);
diff --git a/tests/page-objects/Onboarding/ManualBackupStep1View.ts b/tests/page-objects/Onboarding/ManualBackupStep1View.ts
index c0a2c3a3c17..e98851006f6 100644
--- a/tests/page-objects/Onboarding/ManualBackupStep1View.ts
+++ b/tests/page-objects/Onboarding/ManualBackupStep1View.ts
@@ -1,15 +1,16 @@
import { ManualBackUpStepsSelectorsIDs } from '../../../app/components/Views/ManualBackupStep1/ManualBackUpSteps.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class ManualBackupStep1View {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ManualBackUpStepsSelectorsIDs.STEP_1_CONTAINER,
);
}
- get remindMeLaterButton(): DetoxElement {
+ get remindMeLaterButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ManualBackUpStepsSelectorsIDs.REMIND_ME_LATER_BUTTON,
);
diff --git a/tests/page-objects/Onboarding/MetaMetricsOptInView.ts b/tests/page-objects/Onboarding/MetaMetricsOptInView.ts
index 75f02be8f3c..26aad187ac7 100644
--- a/tests/page-objects/Onboarding/MetaMetricsOptInView.ts
+++ b/tests/page-objects/Onboarding/MetaMetricsOptInView.ts
@@ -15,7 +15,7 @@ import UnifiedGestures from '../../framework/UnifiedGestures';
import { PlaywrightGestures } from '../../framework';
class MetaMetricsOptIn {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
MetaMetricsOptInSelectorsIDs.METAMETRICS_OPT_IN_CONTAINER_ID,
);
@@ -37,7 +37,7 @@ class MetaMetricsOptIn {
});
}
- get optInMetricsContent(): DetoxElement {
+ get optInMetricsContent(): EncapsulatedElementType {
return Matchers.getElementByID(
MetaMetricsOptInSelectorsIDs.OPTIN_METRICS_PRIVACY_POLICY_DESCRIPTION_CONTENT_1_ID,
);
@@ -59,7 +59,7 @@ class MetaMetricsOptIn {
});
}
- get metricsCheckbox(): DetoxElement {
+ get metricsCheckbox(): EncapsulatedElementType {
return Matchers.getElementByID(
MetaMetricsOptInSelectorsIDs.OPTIN_METRICS_METRICS_CHECKBOX,
);
diff --git a/tests/page-objects/Onboarding/OnboardingCarouselView.ts b/tests/page-objects/Onboarding/OnboardingCarouselView.ts
index 24a6ca06aa9..e6158d79797 100644
--- a/tests/page-objects/Onboarding/OnboardingCarouselView.ts
+++ b/tests/page-objects/Onboarding/OnboardingCarouselView.ts
@@ -4,43 +4,44 @@ import {
} from '../../selectors/Onboarding/OnboardingCarousel.selectors';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class OnboardingCarouselView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
OnboardingCarouselSelectorIDs.CAROUSEL_CONTAINER_ID,
);
}
- get getStartedButton(): DetoxElement {
+ get getStartedButton(): EncapsulatedElementType {
return Matchers.getElementByID(
OnboardingCarouselSelectorIDs.GET_STARTED_BUTTON_ID,
);
}
- get titleOne(): DetoxElement {
+ get titleOne(): EncapsulatedElementType {
return Matchers.getElementByText(OnboardingCarouselSelectorText.TITLE_ONE);
}
- get imageOne(): DetoxElement {
+ get imageOne(): EncapsulatedElementType {
return Matchers.getElementByID(OnboardingCarouselSelectorIDs.ONE_IMAGE_ID);
}
- get titleTwo(): DetoxElement {
+ get titleTwo(): EncapsulatedElementType {
return Matchers.getElementByText(OnboardingCarouselSelectorText.TITLE_TWO);
}
- get imageTwo(): DetoxElement {
+ get imageTwo(): EncapsulatedElementType {
return Matchers.getElementByID(OnboardingCarouselSelectorIDs.TWO_IMAGE_ID);
}
- get titleThree(): DetoxElement {
+ get titleThree(): EncapsulatedElementType {
return Matchers.getElementByText(
OnboardingCarouselSelectorText.TITLE_THREE,
);
}
- get imageThree(): DetoxElement {
+ get imageThree(): EncapsulatedElementType {
return Matchers.getElementByID(
OnboardingCarouselSelectorIDs.THREE_IMAGE_ID,
);
diff --git a/tests/page-objects/Onboarding/OnboardingSheet.ts b/tests/page-objects/Onboarding/OnboardingSheet.ts
index 37bda110c76..a11cecb9c00 100644
--- a/tests/page-objects/Onboarding/OnboardingSheet.ts
+++ b/tests/page-objects/Onboarding/OnboardingSheet.ts
@@ -9,7 +9,7 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import UnifiedGestures from '../../framework/UnifiedGestures';
class OnboardingSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(OnboardingSheetSelectorIDs.CONTAINER_ID);
}
diff --git a/tests/page-objects/Onboarding/OnboardingSuccessView.ts b/tests/page-objects/Onboarding/OnboardingSuccessView.ts
index a7e50f12441..caa23aa77db 100644
--- a/tests/page-objects/Onboarding/OnboardingSuccessView.ts
+++ b/tests/page-objects/Onboarding/OnboardingSuccessView.ts
@@ -9,7 +9,7 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import UnifiedGestures from '../../framework/UnifiedGestures';
class OnboardingSuccessView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(OnboardingSuccessSelectorIDs.CONTAINER_ID);
}
diff --git a/tests/page-objects/Onboarding/OnboardingView.ts b/tests/page-objects/Onboarding/OnboardingView.ts
index 0ba1a293b5e..0f91bc5661b 100644
--- a/tests/page-objects/Onboarding/OnboardingView.ts
+++ b/tests/page-objects/Onboarding/OnboardingView.ts
@@ -15,7 +15,7 @@ import {
} from '../../framework';
class OnboardingView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(OnboardingSelectorIDs.CONTAINER_ID);
}
diff --git a/tests/page-objects/Onboarding/ProtectYourWalletModal.ts b/tests/page-objects/Onboarding/ProtectYourWalletModal.ts
index 7994dabb61f..a93a2099d79 100644
--- a/tests/page-objects/Onboarding/ProtectYourWalletModal.ts
+++ b/tests/page-objects/Onboarding/ProtectYourWalletModal.ts
@@ -1,19 +1,20 @@
import { ProtectWalletModalSelectorsIDs } from '../../../app/components/UI/ProtectYourWalletModal/ProtectWalletModal.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class ProtectYourWalletModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ProtectWalletModalSelectorsIDs.CONTAINER);
}
- get remindMeLaterButton(): DetoxElement {
+ get remindMeLaterButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ProtectWalletModalSelectorsIDs.REMIND_ME_LATER_BUTTON,
);
}
- get collapseWalletModal(): DetoxElement {
+ get collapseWalletModal(): EncapsulatedElementType {
return Matchers.getElementByID(
ProtectWalletModalSelectorsIDs.COLLAPSED_WALLET_MODAL,
);
diff --git a/tests/page-objects/Onboarding/ProtectYourWalletView.ts b/tests/page-objects/Onboarding/ProtectYourWalletView.ts
index c477e1dceb6..070bfc004c7 100644
--- a/tests/page-objects/Onboarding/ProtectYourWalletView.ts
+++ b/tests/page-objects/Onboarding/ProtectYourWalletView.ts
@@ -13,7 +13,7 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import { PlatformDetector } from '../../framework/PlatformLocator';
class ProtectYourWalletView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ManualBackUpStepsSelectorsIDs.PROTECT_CONTAINER,
);
diff --git a/tests/page-objects/Onboarding/WhatsNewModal.ts b/tests/page-objects/Onboarding/WhatsNewModal.ts
index c2842772a27..ae5d729d8ab 100644
--- a/tests/page-objects/Onboarding/WhatsNewModal.ts
+++ b/tests/page-objects/Onboarding/WhatsNewModal.ts
@@ -1,13 +1,14 @@
import { WhatsNewModalSelectorsIDs } from '../../../app/components/UI/WhatsNewModal/WhatsNewModal.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class WhatsNewModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(WhatsNewModalSelectorsIDs.CONTAINER);
}
- get closeButton(): DetoxElement {
+ get closeButton(): EncapsulatedElementType {
return Matchers.getElementByID(WhatsNewModalSelectorsIDs.CLOSE_BUTTON);
}
diff --git a/tests/page-objects/Perps/PerpsDepositProcessingView.ts b/tests/page-objects/Perps/PerpsDepositProcessingView.ts
index e90d7e06a8d..59a8eee05e1 100644
--- a/tests/page-objects/Perps/PerpsDepositProcessingView.ts
+++ b/tests/page-objects/Perps/PerpsDepositProcessingView.ts
@@ -2,27 +2,28 @@ import { PerpsDepositProcessingViewSelectorsIDs } from '../../../app/components/
import Matchers from '../../framework/Matchers';
import Assertions from '../../framework/Assertions';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class PerpsDepositProcessingView {
- get headerTitle(): DetoxElement {
+ get headerTitle(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsDepositProcessingViewSelectorsIDs.HEADER_TITLE,
);
}
- get statusTitle(): DetoxElement {
+ get statusTitle(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsDepositProcessingViewSelectorsIDs.STATUS_TITLE,
);
}
- get statusDescription(): DetoxElement {
+ get statusDescription(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsDepositProcessingViewSelectorsIDs.STATUS_DESCRIPTION,
);
}
- get viewBalanceButton(): DetoxElement {
+ get viewBalanceButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsDepositProcessingViewSelectorsIDs.VIEW_BALANCE_BUTTON,
);
diff --git a/tests/page-objects/Perps/PerpsDepositView.ts b/tests/page-objects/Perps/PerpsDepositView.ts
index 6f255d4703f..69fa8028b7d 100644
--- a/tests/page-objects/Perps/PerpsDepositView.ts
+++ b/tests/page-objects/Perps/PerpsDepositView.ts
@@ -21,7 +21,7 @@ const TIMEOUT = {
class PerpsDepositView {
// Custom deposit keypad container
- get keypad(): DetoxElement {
+ get keypad(): EncapsulatedElementType {
return Matchers.getElementByID('deposit-keyboard');
}
@@ -63,7 +63,7 @@ class PerpsDepositView {
// Add funds (confirm) button on review screen. Uses testID for reliability:
// the confirmation screen shows at most one "Add funds" (ConfirmButton);
// index 1 was failing when no second "Add funds" existed in the hierarchy.
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID('confirm-button');
}
@@ -84,7 +84,7 @@ class PerpsDepositView {
});
}
- get usdcOption(): DetoxElement {
+ get usdcOption(): EncapsulatedElementType {
return Matchers.getElementByText('USDC');
}
diff --git a/tests/page-objects/Perps/PerpsHomeView.ts b/tests/page-objects/Perps/PerpsHomeView.ts
index b602bf47fff..f0cf10ae358 100644
--- a/tests/page-objects/Perps/PerpsHomeView.ts
+++ b/tests/page-objects/Perps/PerpsHomeView.ts
@@ -3,13 +3,14 @@ import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import Utilities from '../../framework/Utilities';
import enContent from '../../../locales/languages/en.json';
+import { EncapsulatedElementType } from '../../framework';
class PerpsHomeView {
- get exploreCrypto(): DetoxElement {
+ get exploreCrypto(): EncapsulatedElementType {
return Matchers.getElementByText(enContent.perps.home.crypto);
}
- get backHome(): DetoxElement {
+ get backHome(): EncapsulatedElementType {
return Matchers.getElementByID(PerpsHomeViewSelectorsIDs.BACK_HOME_BUTTON);
}
diff --git a/tests/page-objects/Perps/PerpsMarketDetailsView.ts b/tests/page-objects/Perps/PerpsMarketDetailsView.ts
index 456e59d4560..d0969cbebfb 100644
--- a/tests/page-objects/Perps/PerpsMarketDetailsView.ts
+++ b/tests/page-objects/Perps/PerpsMarketDetailsView.ts
@@ -133,7 +133,7 @@ class PerpsMarketDetailsView {
}
// Scroll view
- get scrollView(): DetoxElement {
+ get scrollView(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsMarketDetailsViewSelectorsIDs.SCROLL_VIEW,
);
diff --git a/tests/page-objects/Perps/PerpsMarketListView.ts b/tests/page-objects/Perps/PerpsMarketListView.ts
index 1516ddebdf7..3551700ddc2 100644
--- a/tests/page-objects/Perps/PerpsMarketListView.ts
+++ b/tests/page-objects/Perps/PerpsMarketListView.ts
@@ -34,7 +34,7 @@ class PerpsMarketListView {
* HeaderCompactStandard back on explore market list (see PerpsMarketListView.tsx).
* Navigates from the market list back to Perps portfolio home.
*/
- get headerBackButton(): DetoxElement {
+ get headerBackButton(): EncapsulatedElementType {
return Matchers.getElementByID(
`${PerpsMarketListViewSelectorsIDs.CLOSE_BUTTON}-back-button`,
);
@@ -108,7 +108,6 @@ class PerpsMarketListView {
// Generic selector for first market row item (regardless of coin)
get firstMarketRowItem() {
- // Match any element with testID that starts with 'perps-market-row-item-' and get the first one
return Matchers.getElementByID(/^perps-market-row-item-.*/, 0);
}
diff --git a/tests/page-objects/Perps/PerpsOrderView.ts b/tests/page-objects/Perps/PerpsOrderView.ts
index 9e66a2af815..a9ceb576502 100644
--- a/tests/page-objects/Perps/PerpsOrderView.ts
+++ b/tests/page-objects/Perps/PerpsOrderView.ts
@@ -64,7 +64,7 @@ class PerpsOrderView {
}
// Leverage chip by visible text, e.g., "3x", "10x", "20x"
- leverageOption(leverageX: number, index = 0): DetoxElement {
+ leverageOption(leverageX: number, index = 0): EncapsulatedElementType {
return Matchers.getElementByText(`${leverageX}x`, index);
}
@@ -77,7 +77,7 @@ class PerpsOrderView {
}
// Modal title to ensure the leverage bottom sheet is visible
- get leverageModalTitle(): DetoxElement {
+ get leverageModalTitle(): EncapsulatedElementType {
return Matchers.getElementByText('Set Leverage');
}
@@ -283,16 +283,16 @@ class PerpsOrderView {
}
// Order type / Limit Price helpers
- private get orderTypeMarket(): DetoxElement {
+ private get orderTypeMarket(): EncapsulatedElementType {
return Matchers.getElementByText('Market');
}
- private get orderTypeSelector(): DetoxElement {
+ private get orderTypeSelector(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsOrderHeaderSelectorsIDs.ORDER_TYPE_BUTTON,
);
}
- private get orderTypeLimit(): DetoxElement {
+ private get orderTypeLimit(): EncapsulatedElementType {
return Matchers.getElementByText('Limit');
}
diff --git a/tests/page-objects/Perps/PerpsView.ts b/tests/page-objects/Perps/PerpsView.ts
index 3e0b0c6e4a6..d1d2027a212 100644
--- a/tests/page-objects/Perps/PerpsView.ts
+++ b/tests/page-objects/Perps/PerpsView.ts
@@ -14,6 +14,7 @@ import Utilities from '../../framework/Utilities';
import { waitForStableEnabledIOS } from './waitForStableEnabledIOS';
import PerpsHomeView from './PerpsHomeView';
import PerpsMarketListView from './PerpsMarketListView';
+import { EncapsulatedElementType } from '../../framework';
/** Portfolio: limit order primary (`formatOrderLabel`) + position primary (`{symbol} {n}x {side}`). */
export interface PerpsPortfolioLimitFlowExpectOptions {
@@ -35,7 +36,7 @@ class PerpsView {
leverageX: number,
direction: 'long' | 'short',
index = 0,
- ): DetoxElement {
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
new RegExp(
`^perps-positions-item-${symbol}-${leverageX}x-${direction}-${index}$`,
@@ -51,7 +52,7 @@ class PerpsView {
symbol: string,
direction: 'long' | 'short',
index = 0,
- ): DetoxElement {
+ ): EncapsulatedElementType {
const escapedSymbol = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return Matchers.getElementByID(
new RegExp(
@@ -67,11 +68,11 @@ class PerpsView {
}
// "Edit TP/SL" button visible on position details
- get editTpslButton(): DetoxElement {
+ get editTpslButton(): EncapsulatedElementType {
return Matchers.getElementByText('Edit TP/SL');
}
- get closePositionBottomSheetButton(): DetoxElement {
+ get closePositionBottomSheetButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON,
);
@@ -105,21 +106,21 @@ class PerpsView {
return Matchers.getElementByText('Dismiss');
}
- get anchor(): DetoxElement {
+ get anchor(): EncapsulatedElementType {
return Matchers.getElementByID('perps-tab-scroll-view');
}
/** Perps home header — use as swipe target when {@link anchor} is absent. */
- get perpsHomeHeader(): DetoxElement {
+ get perpsHomeHeader(): EncapsulatedElementType {
return Matchers.getElementByID('perps-home');
}
// Orders section on the Perps main tab
- get ordersSectionTitle(): DetoxElement {
+ get ordersSectionTitle(): EncapsulatedElementType {
return Matchers.getElementByText('Orders');
}
- get anyOrderCardOnTab(): DetoxElement {
+ get anyOrderCardOnTab(): EncapsulatedElementType {
// PerpsCard has no specific testID for orders; assert by the presence of the title and any text matching limit label
return Matchers.getElementByText('Limit');
}
diff --git a/tests/page-objects/Predict/PredictAddFunds.ts b/tests/page-objects/Predict/PredictAddFunds.ts
index cb872443f98..dda3e74734c 100644
--- a/tests/page-objects/Predict/PredictAddFunds.ts
+++ b/tests/page-objects/Predict/PredictAddFunds.ts
@@ -1,8 +1,8 @@
-import { Matchers, Gestures } from '../../framework';
+import { Matchers, Gestures, EncapsulatedElementType } from '../../framework';
import { PredictAddFundsSelectorText } from '../../../app/components/UI/Predict/Predict.testIds';
class PredictAddFunds {
- get addFundsButton(): DetoxElement {
+ get addFundsButton(): EncapsulatedElementType {
return Matchers.getElementByText(PredictAddFundsSelectorText.ADD_FUNDS);
}
diff --git a/tests/page-objects/Predict/PredictBalance.ts b/tests/page-objects/Predict/PredictBalance.ts
index 54836b1d188..a2083fb1038 100644
--- a/tests/page-objects/Predict/PredictBalance.ts
+++ b/tests/page-objects/Predict/PredictBalance.ts
@@ -1,12 +1,18 @@
-import { Matchers, Gestures, Assertions, Utilities } from '../../framework';
+import {
+ Matchers,
+ Gestures,
+ Assertions,
+ Utilities,
+ EncapsulatedElementType,
+} from '../../framework';
import { PredictBalanceSelectorsIDs } from '../../../app/components/UI/Predict/Predict.testIds';
class PredictBalance {
- get balanceCard(): DetoxElement {
+ get balanceCard(): EncapsulatedElementType {
return Matchers.getElementByID(PredictBalanceSelectorsIDs.BALANCE_CARD);
}
- get withdrawButton(): DetoxElement {
+ get withdrawButton(): EncapsulatedElementType {
return Matchers.getElementByID(PredictBalanceSelectorsIDs.WITHDRAW_BUTTON);
}
diff --git a/tests/page-objects/Predict/PredictCashOutPage.ts b/tests/page-objects/Predict/PredictCashOutPage.ts
index b05b7e89648..66f70222588 100644
--- a/tests/page-objects/Predict/PredictCashOutPage.ts
+++ b/tests/page-objects/Predict/PredictCashOutPage.ts
@@ -1,11 +1,11 @@
-import { Matchers, Gestures } from '../../framework';
+import { Matchers, Gestures, EncapsulatedElementType } from '../../framework';
import { PredictCashOutSelectorsIDs } from '../../../app/components/UI/Predict/Predict.testIds';
class PredictCashOutPage {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(PredictCashOutSelectorsIDs.CONTAINER);
}
- get cashOutButton(): DetoxElement {
+ get cashOutButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictCashOutSelectorsIDs.SELL_PREVIEW_CASH_OUT_BUTTON,
);
diff --git a/tests/page-objects/Predict/PredictClaimPage.ts b/tests/page-objects/Predict/PredictClaimPage.ts
index 35259ca14b7..5478e61e312 100644
--- a/tests/page-objects/Predict/PredictClaimPage.ts
+++ b/tests/page-objects/Predict/PredictClaimPage.ts
@@ -1,13 +1,13 @@
-import { Matchers, Gestures } from '../../framework';
+import { Matchers, Gestures, EncapsulatedElementType } from '../../framework';
import { PredictClaimConfirmationSelectorsIDs } from '../../../app/components/UI/Predict/Predict.testIds';
class PredictClaimPage {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictClaimConfirmationSelectorsIDs.CLAIM_BACKGROUND_CONTAINER,
);
}
- get claimConfirmButton(): DetoxElement {
+ get claimConfirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictClaimConfirmationSelectorsIDs.CLAIM_CONFIRM_BUTTON,
);
diff --git a/tests/page-objects/Predict/PredictUnavailableView.ts b/tests/page-objects/Predict/PredictUnavailableView.ts
index 34be4fcd994..5cf209fd127 100644
--- a/tests/page-objects/Predict/PredictUnavailableView.ts
+++ b/tests/page-objects/Predict/PredictUnavailableView.ts
@@ -2,19 +2,20 @@ import Assertions from '../../framework/Assertions';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import { PredictUnavailableSelectorsIDs } from '../../../app/components/UI/Predict/Predict.testIds';
+import { EncapsulatedElementType } from '../../framework';
class PredictUnavailableView {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(PredictUnavailableSelectorsIDs.TITLE_TEXT);
}
- get description(): DetoxElement {
+ get description(): EncapsulatedElementType {
return Matchers.getElementByText(
PredictUnavailableSelectorsIDs.DESCRIPTION_TEXT,
);
}
- get gotItButton(): DetoxElement {
+ get gotItButton(): EncapsulatedElementType {
return Matchers.getElementByText(
PredictUnavailableSelectorsIDs.BUTTON_TEXT,
);
diff --git a/tests/page-objects/Ramps/BuildQuoteView.ts b/tests/page-objects/Ramps/BuildQuoteView.ts
index 1d25c3b8c69..4521929bcfd 100644
--- a/tests/page-objects/Ramps/BuildQuoteView.ts
+++ b/tests/page-objects/Ramps/BuildQuoteView.ts
@@ -4,91 +4,92 @@ import Utilities from '../../framework/Utilities';
import { BuildQuoteSelectors } from '../../../app/components/UI/Ramp/Aggregator/Views/BuildQuote/BuildQuote.testIds';
import { AddressSelectorSelectors } from '../../../app/components/Views/AddressSelector/AddressSelector.testIds';
+import { EncapsulatedElementType } from '../../framework';
class BuildQuoteView {
- get amountToBuyLabel(): DetoxElement {
+ get amountToBuyLabel(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.AMOUNT_TO_BUY_LABEL);
}
- get amountToSellLabel(): DetoxElement {
+ get amountToSellLabel(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.AMOUNT_TO_SELL_LABEL);
}
- get getQuotesButton(): DetoxElement {
+ get getQuotesButton(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.GET_QUOTES_BUTTON);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.CANCEL_BUTTON_TEXT);
}
- get selectRegionDropdown(): DetoxElement {
+ get selectRegionDropdown(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.SELECT_REGION);
}
- get selectPaymentMethodDropdown(): DetoxElement {
+ get selectPaymentMethodDropdown(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.SELECT_PAYMENT_METHOD);
}
- get selectCurrencyDropdown(): DetoxElement {
+ get selectCurrencyDropdown(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.SELECT_CURRENCY);
}
- get amountInput(): DetoxElement {
+ get amountInput(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.AMOUNT_INPUT);
}
- get regionDropdown(): DetoxElement {
+ get regionDropdown(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.REGION_DROPDOWN);
}
- get accountPicker(): DetoxElement {
+ get accountPicker(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.ACCOUNT_PICKER);
}
- get minLimitErrorMessage(): DetoxElement {
+ get minLimitErrorMessage(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.MIN_LIMIT_ERROR);
}
- get maxLimitErrorMessage(): DetoxElement {
+ get maxLimitErrorMessage(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.MAX_LIMIT_ERROR);
}
- get insufficientBalanceErrorMessage(): DetoxElement {
+ get insufficientBalanceErrorMessage(): EncapsulatedElementType {
return Matchers.getElementByID(
BuildQuoteSelectors.INSUFFICIENT_BALANCE_ERROR,
);
}
- get keypadDeleteButton(): DetoxElement {
+ get keypadDeleteButton(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.KEYPAD_DELETE_BUTTON);
}
- get doneButton(): DetoxElement {
+ get doneButton(): EncapsulatedElementType {
return Matchers.getElementByText(BuildQuoteSelectors.DONE_BUTTON);
}
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByID(BuildQuoteSelectors.CONTINUE_BUTTON);
}
- get quickAmount25(): DetoxElement {
+ get quickAmount25(): EncapsulatedElementType {
return Matchers.getElementByLabel('25%');
}
- get quickAmount50(): DetoxElement {
+ get quickAmount50(): EncapsulatedElementType {
return Matchers.getElementByLabel('50%');
}
- get quickAmount75(): DetoxElement {
+ get quickAmount75(): EncapsulatedElementType {
return Matchers.getElementByLabel('75%');
}
- get quickAmountMax(): DetoxElement {
+ get quickAmountMax(): EncapsulatedElementType {
return Matchers.getElementByLabel('MAX');
}
- get accountPickerDropdown(): DetoxElement {
+ get accountPickerDropdown(): EncapsulatedElementType {
return Matchers.getElementByID(
AddressSelectorSelectors.ACCOUNT_PICKER_DROPDOWN,
);
@@ -188,7 +189,10 @@ class BuildQuoteView {
async tapPaymentMethodDropdown(
paymentMethod: string | RegExp,
): Promise {
- const paymentMethodOption = Matchers.getElementByText(paymentMethod);
+ const paymentMethodOption =
+ typeof paymentMethod === 'string'
+ ? Matchers.getElementByText(paymentMethod)
+ : Matchers.getElementByText(paymentMethod as RegExp);
await Gestures.waitAndTap(paymentMethodOption, {
elemDescription: `Payment Method Dropdown (${paymentMethod}) in Build Quote View`,
});
@@ -198,7 +202,9 @@ class BuildQuoteView {
regex: string | RegExp,
): Promise {
try {
- const elem = await Matchers.getElementByText(regex);
+ const elem = await (typeof regex === 'string'
+ ? Matchers.getElementByText(regex)
+ : Matchers.getElementByText(regex as RegExp));
const attributes = await (
elem as unknown as IndexableNativeElement
).getAttributes();
diff --git a/tests/page-objects/Ramps/BuyGetStartedView.ts b/tests/page-objects/Ramps/BuyGetStartedView.ts
index f89e6ecf7c1..3a4f5e50647 100644
--- a/tests/page-objects/Ramps/BuyGetStartedView.ts
+++ b/tests/page-objects/Ramps/BuyGetStartedView.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { GetStartedSelectors } from '../../selectors/Ramps/GetStarted.selectors';
+import { EncapsulatedElementType } from '../../framework';
class BuyGetStartedView {
- get getStartedButton(): DetoxElement {
+ get getStartedButton(): EncapsulatedElementType {
return Matchers.getElementByText(GetStartedSelectors.GET_STARTED);
}
diff --git a/tests/page-objects/Ramps/KYCScreen.ts b/tests/page-objects/Ramps/KYCScreen.ts
index 3ce9aa09743..123cce6fcce 100644
--- a/tests/page-objects/Ramps/KYCScreen.ts
+++ b/tests/page-objects/Ramps/KYCScreen.ts
@@ -3,25 +3,25 @@ import { OtpCodeSelectorsIDs } from '../../../app/components/UI/Ramp/Views/Nativ
import { VerifyIdentitySelectorsIDs } from '../../../app/components/UI/Ramp/Views/NativeFlow/VerifyIdentity.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
-import { Utilities } from '../../framework';
+import { Utilities, EncapsulatedElementType } from '../../framework';
class KYCScreen {
- get verifyIdentityContinueButton(): DetoxElement {
+ get verifyIdentityContinueButton(): EncapsulatedElementType {
return Matchers.getElementByID(VerifyIdentitySelectorsIDs.CONTINUE_BUTTON);
}
- get emailInput(): DetoxElement {
+ get emailInput(): EncapsulatedElementType {
return Matchers.getElementByID(EnterEmailSelectorsIDs.EMAIL_INPUT);
}
- get sendEmailButton(): DetoxElement {
+ get sendEmailButton(): EncapsulatedElementType {
return Matchers.getElementByID(EnterEmailSelectorsIDs.SEND_EMAIL_BUTTON);
}
- get otpScreen(): DetoxElement {
+ get otpScreen(): EncapsulatedElementType {
return Matchers.getElementByID(OtpCodeSelectorsIDs.OTP_CODE_SCREEN);
}
- get otpCodeInput(): DetoxElement {
+ get otpCodeInput(): EncapsulatedElementType {
return Matchers.getElementByID(OtpCodeSelectorsIDs.OTP_CODE_INPUT);
}
diff --git a/tests/page-objects/Ramps/OrderDetailsView.ts b/tests/page-objects/Ramps/OrderDetailsView.ts
index deec2a925b7..76b94e0a3d2 100644
--- a/tests/page-objects/Ramps/OrderDetailsView.ts
+++ b/tests/page-objects/Ramps/OrderDetailsView.ts
@@ -2,20 +2,21 @@ import { RampsOrderDetailsSelectorsIDs } from '../../../app/components/UI/Ramp/V
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import Utilities from '../../framework/Utilities';
+import { EncapsulatedElementType } from '../../framework';
class OrderDetailsView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(RampsOrderDetailsSelectorsIDs.CONTAINER);
}
- get closeButton(): DetoxElement {
+ get closeButton(): EncapsulatedElementType {
return Matchers.getElementByID(RampsOrderDetailsSelectorsIDs.CLOSE_BUTTON);
}
- get tokenAmount(): DetoxElement {
+ get tokenAmount(): EncapsulatedElementType {
return Matchers.getElementByID(RampsOrderDetailsSelectorsIDs.TOKEN_AMOUNT);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(RampsOrderDetailsSelectorsIDs.BACK_BUTTON);
}
diff --git a/tests/page-objects/Ramps/QuotesView.ts b/tests/page-objects/Ramps/QuotesView.ts
index 11ac34f9de0..927a14bec10 100644
--- a/tests/page-objects/Ramps/QuotesView.ts
+++ b/tests/page-objects/Ramps/QuotesView.ts
@@ -1,29 +1,30 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { QuoteSelectors } from '../../../app/components/UI/Ramp/Aggregator/Views/Quotes/Quotes.testIds';
+import { EncapsulatedElementType } from '../../framework';
class QuotesView {
- get selectAQuoteLabel(): DetoxElement {
+ get selectAQuoteLabel(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteSelectors.RECOMMENDED_QUOTE);
}
- get quoteAmountLabel(): DetoxElement {
+ get quoteAmountLabel(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteSelectors.QUOTE_AMOUNT_LABEL);
}
- get quotes(): DetoxElement {
+ get quotes(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteSelectors.QUOTES);
}
- get exploreMoreOptions(): DetoxElement {
+ get exploreMoreOptions(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteSelectors.EXPLORE_MORE_OPTIONS);
}
- get expandedQuotesSection(): DetoxElement {
+ get expandedQuotesSection(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteSelectors.EXPANDED_QUOTES_SECTION);
}
- get continueWithProvider(): DetoxElement {
+ get continueWithProvider(): EncapsulatedElementType {
const providerLocator = QuoteSelectors.CONTINUE_WITH_PROVIDER.replace(
'{{provider}}',
'.*',
diff --git a/tests/page-objects/Ramps/SelectPaymentMethodView.ts b/tests/page-objects/Ramps/SelectPaymentMethodView.ts
index cad8b154d44..145a879c372 100644
--- a/tests/page-objects/Ramps/SelectPaymentMethodView.ts
+++ b/tests/page-objects/Ramps/SelectPaymentMethodView.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { SelectPaymentMethodSelectors } from '../../selectors/Ramps/SelectPaymentMethod.selectors';
+import { EncapsulatedElementType } from '../../framework';
class SelectPaymentMethodView {
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByText(
SelectPaymentMethodSelectors.CONTINUE_BUTTON,
);
diff --git a/tests/page-objects/Ramps/SelectRegionView.ts b/tests/page-objects/Ramps/SelectRegionView.ts
index a1fe0fa0b9c..09f86a35d7f 100644
--- a/tests/page-objects/Ramps/SelectRegionView.ts
+++ b/tests/page-objects/Ramps/SelectRegionView.ts
@@ -1,13 +1,14 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { SelectRegionSelectors } from '../../selectors/Ramps/SelectRegion.selectors';
+import { EncapsulatedElementType } from '../../framework';
class SelectRegionView {
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByText(SelectRegionSelectors.CONTINUE_BUTTON);
}
- get regionSearchInput(): DetoxElement {
+ get regionSearchInput(): EncapsulatedElementType {
return Matchers.getElementByID(
SelectRegionSelectors.REGION_MODAL_SEARCH_INPUT,
);
diff --git a/tests/page-objects/Ramps/SellGetStartedView.ts b/tests/page-objects/Ramps/SellGetStartedView.ts
index 513351ba0b2..6f43c980251 100644
--- a/tests/page-objects/Ramps/SellGetStartedView.ts
+++ b/tests/page-objects/Ramps/SellGetStartedView.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { GetStartedSelectors } from '../../selectors/Ramps/GetStarted.selectors';
+import { EncapsulatedElementType } from '../../framework';
class SellGetStartedView {
- get getStartedButton(): DetoxElement {
+ get getStartedButton(): EncapsulatedElementType {
return Matchers.getElementByText(GetStartedSelectors.GET_STARTED);
}
diff --git a/tests/page-objects/Ramps/TokenSelectScreen.ts b/tests/page-objects/Ramps/TokenSelectScreen.ts
index c6feebd2d3e..69056235f77 100644
--- a/tests/page-objects/Ramps/TokenSelectScreen.ts
+++ b/tests/page-objects/Ramps/TokenSelectScreen.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { selectTokenSelectors } from '../../../app/components/UI/Ramp/Aggregator/components/TokenSelectModal/SelectToken.testIds';
+import { EncapsulatedElementType } from '../../framework';
class TokenSelectScreen {
- get tokenSearchInput(): DetoxElement {
+ get tokenSearchInput(): EncapsulatedElementType {
return Matchers.getElementByID(
selectTokenSelectors.TOKEN_SELECT_MODAL_SEARCH_INPUT,
);
diff --git a/tests/page-objects/Send/AddAddressModal.ts b/tests/page-objects/Send/AddAddressModal.ts
index ddc94b3a64d..23b3d83bb4a 100644
--- a/tests/page-objects/Send/AddAddressModal.ts
+++ b/tests/page-objects/Send/AddAddressModal.ts
@@ -1,25 +1,26 @@
import { AddAddressModalSelectorsIDs } from '../../../app/components/UI/AddToAddressBookWrapper/AddAddressModal.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class AddAddressModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(AddAddressModalSelectorsIDs.CONTAINER);
}
- get aliasInput(): DetoxElement {
+ get aliasInput(): EncapsulatedElementType {
return Matchers.getElementByID(
AddAddressModalSelectorsIDs.ENTER_ALIAS_INPUT,
);
}
- get saveButton(): DetoxElement {
+ get saveButton(): EncapsulatedElementType {
return device.getPlatform() === 'android'
? Matchers.getElementByLabel(AddAddressModalSelectorsIDs.SAVE_BUTTON)
: Matchers.getElementByID(AddAddressModalSelectorsIDs.SAVE_BUTTON);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByID(AddAddressModalSelectorsIDs.TITLE);
}
diff --git a/tests/page-objects/Send/RedesignedSendView.ts b/tests/page-objects/Send/RedesignedSendView.ts
index 24e80314872..2d8bd5fd8ea 100644
--- a/tests/page-objects/Send/RedesignedSendView.ts
+++ b/tests/page-objects/Send/RedesignedSendView.ts
@@ -1,7 +1,11 @@
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import { RedesignedSendViewSelectorsIDs } from '../../../app/components/Views/confirmations/components/send/RedesignedSendView.testIds';
-import { Utilities, Assertions } from '../../framework';
+import {
+ Utilities,
+ Assertions,
+ EncapsulatedElementType,
+} from '../../framework';
import { CommonSelectorsIDs } from '../../../app/util/Common.testIds';
import { SendActionViewSelectorsIDs } from '../../selectors/SendFlow/SendActionView.selectors';
import { encapsulatedAction } from '../../framework/encapsulatedAction';
@@ -12,69 +16,69 @@ import { PlatformDetector } from '../../framework/PlatformLocator';
import { getNetworkFilterTestId } from '../../../app/components/Views/confirmations/components/network-filter/network-filter.testIds';
class SendView {
- get ethereumTokenButton(): DetoxElement {
+ get ethereumTokenButton(): EncapsulatedElementType {
return Matchers.getElementByText('Ethereum');
}
- get erc20TokenButton(): DetoxElement {
+ get erc20TokenButton(): EncapsulatedElementType {
return Matchers.getElementByText('USD Coin');
}
- get zeroButton(): DetoxElement {
+ get zeroButton(): EncapsulatedElementType {
return Matchers.getElementByText('0', 1);
}
- get amountFiveButton(): DetoxElement {
+ get amountFiveButton(): EncapsulatedElementType {
return Matchers.getElementByText('5');
}
- get fiftyPercentButton(): DetoxElement {
+ get fiftyPercentButton(): EncapsulatedElementType {
return Matchers.getElementByID('percentage-button-50');
}
- get maxButton(): DetoxElement {
+ get maxButton(): EncapsulatedElementType {
return Matchers.getElementByID('percentage-button-100');
}
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByText('Continue');
}
- get recipientAddressInput(): DetoxElement {
+ get recipientAddressInput(): EncapsulatedElementType {
return Matchers.getElementByID(
RedesignedSendViewSelectorsIDs.RECIPIENT_ADDRESS_INPUT,
);
}
- get reviewButton(): DetoxElement {
+ get reviewButton(): EncapsulatedElementType {
return Matchers.getElementByID(
RedesignedSendViewSelectorsIDs.REVIEW_BUTTON,
);
}
- get amountInputField(): DetoxElement {
+ get amountInputField(): EncapsulatedElementType {
return Matchers.getElementByID('txn-amount-input');
}
- get nextButton(): DetoxElement {
+ get nextButton(): EncapsulatedElementType {
return Matchers.getElementByID('txn-amount-next-button');
}
- get currencySwitch(): DetoxElement {
+ get currencySwitch(): EncapsulatedElementType {
return Matchers.getElementByID('amount-screen-currency-switch');
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.BACK_ARROW_BUTTON);
}
- get insufficientBalanceToCoverFeesError(): DetoxElement {
+ get insufficientBalanceToCoverFeesError(): EncapsulatedElementType {
return Matchers.getElementByText(
SendActionViewSelectorsIDs.INSUFFICIENT_BALANCE_TO_COVER_FEES_ERROR,
);
}
- get insufficientFundsError(): DetoxElement {
+ get insufficientFundsError(): EncapsulatedElementType {
return Matchers.getElementByText(
SendActionViewSelectorsIDs.INSUFFICIENT_FUNDS_ERROR,
);
diff --git a/tests/page-objects/Send/TransactionConfirmView.ts b/tests/page-objects/Send/TransactionConfirmView.ts
index 9fb8b53aa45..13cbec55ee1 100644
--- a/tests/page-objects/Send/TransactionConfirmView.ts
+++ b/tests/page-objects/Send/TransactionConfirmView.ts
@@ -15,6 +15,7 @@ import {
// TransactionConfirmViewSelectorsText,
// } from '../../../app/components/Views/confirmations/legacy/components/Confirm/TransactionConfirmView.testIds';
import RowComponents from '../Browser/Confirmations/RowComponents';
+import { EncapsulatedElementType } from '../../framework';
// Temporary placeholders to prevent TypeScript errors
const TransactionConfirmViewSelectorsIDs = {
@@ -29,7 +30,7 @@ const TransactionConfirmViewSelectorsText = {
};
class TransactionConfirmationView {
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(
TransactionConfirmViewSelectorsIDs.CONFIRM_TRANSACTION_BUTTON_ID,
@@ -39,68 +40,68 @@ class TransactionConfirmationView {
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByText(
TransactionConfirmViewSelectorsText.CANCEL_BUTTON,
);
}
- get maxGasFee(): DetoxElement {
+ get maxGasFee(): EncapsulatedElementType {
return Matchers.getElementByID(
EditGasViewSelectorsIDs.MAX_PRIORITY_FEE_INPUT_TEST_ID,
);
}
- get editPriorityFeeSheetContainer(): DetoxElement {
+ get editPriorityFeeSheetContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
EditGasViewSelectorsIDs.EDIT_PRIORITY_SCREEN_TEST_ID,
);
}
- get transactionAmount(): DetoxElement {
+ get transactionAmount(): EncapsulatedElementType {
return Matchers.getElementByID(
TransactionConfirmViewSelectorsIDs.CONFIRM_TXN_AMOUNT,
);
}
- get estimatedGasLink(): DetoxElement {
+ get estimatedGasLink(): EncapsulatedElementType {
return Matchers.getElementByID(
EditGasViewSelectorsIDs.ESTIMATED_FEE_TEST_ID,
);
}
- get transactionViewContainer(): DetoxElement {
+ get transactionViewContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
TransactionConfirmViewSelectorsIDs.TRANSACTION_VIEW_CONTAINER_ID,
);
}
- get LowPriorityText(): DetoxElement {
+ get LowPriorityText(): EncapsulatedElementType {
return Matchers.getElementByText(EditGasViewSelectorsText.LOW);
}
- get MarketPriorityText(): DetoxElement {
+ get MarketPriorityText(): EncapsulatedElementType {
return Matchers.getElementByText(EditGasViewSelectorsText.MARKET);
}
- get AggressivePriorityText(): DetoxElement {
+ get AggressivePriorityText(): EncapsulatedElementType {
return Matchers.getElementByText(EditGasViewSelectorsText.AGGRESSIVE);
}
- get EditPrioritySaveButtonText(): DetoxElement {
+ get EditPrioritySaveButtonText(): EncapsulatedElementType {
return Matchers.getElementByText(EditGasViewSelectorsText.SAVE_BUTTON);
}
- get EditPriorityAdvancedOptionsText(): DetoxElement {
+ get EditPriorityAdvancedOptionsText(): EncapsulatedElementType {
return Matchers.getElementByText(EditGasViewSelectorsText.ADVANCE_OPTIONS);
}
- get editPriorityLegacyModal(): DetoxElement {
+ get editPriorityLegacyModal(): EncapsulatedElementType {
return Matchers.getElementByID(EditGasViewSelectorsIDs.LEGACY_CONTAINER);
}
- get securityAlertBanner(): DetoxElement {
+ get securityAlertBanner(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationTopSheetSelectorsIDs.SECURITY_ALERT_BANNER,
);
}
- get securityAlertResponseFailedBanner(): DetoxElement {
+ get securityAlertResponseFailedBanner(): EncapsulatedElementType {
return Matchers.getElementByID(
ConfirmationTopSheetSelectorsIDs.SECURITY_ALERT_RESPONSE_FAILED_BANNER,
);
diff --git a/tests/page-objects/Settings/Advanced/FiatOnTestnetsBottomSheet.ts b/tests/page-objects/Settings/Advanced/FiatOnTestnetsBottomSheet.ts
index 9e372c20871..989ad2b3ee2 100644
--- a/tests/page-objects/Settings/Advanced/FiatOnTestnetsBottomSheet.ts
+++ b/tests/page-objects/Settings/Advanced/FiatOnTestnetsBottomSheet.ts
@@ -1,9 +1,10 @@
import { FiatOnTestnetsBottomSheetSelectorsIDs } from '../../../../app/components/Views/Settings/AdvancedSettings/FiatOnTestnetsFriction/FiatOnTestnetsBottomSheet.testIds';
import Gestures from '../../../framework/Gestures';
import Matchers from '../../../framework/Matchers';
+import { EncapsulatedElementType } from '../../../framework';
class FiatOnTestnetsBottomSheet {
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByID(
FiatOnTestnetsBottomSheetSelectorsIDs.CONTINUE_BUTTON,
);
diff --git a/tests/page-objects/Settings/AdvancedView.ts b/tests/page-objects/Settings/AdvancedView.ts
index 748455d182d..522c412fcfa 100644
--- a/tests/page-objects/Settings/AdvancedView.ts
+++ b/tests/page-objects/Settings/AdvancedView.ts
@@ -4,6 +4,7 @@ import {
} from '../../../app/components/Views/Settings/AdvancedSettings/AdvancedView.testIds';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class AdvancedSettingsView {
get scrollViewIdentifier(): Promise {
@@ -12,24 +13,24 @@ class AdvancedSettingsView {
);
}
- get showFiatOnTestnetsToggle(): DetoxElement {
+ get showFiatOnTestnetsToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
AdvancedViewSelectorsIDs.SHOW_FIAT_ON_TESTNETS,
);
}
- get smartTransactionsToggle(): DetoxElement {
+ get smartTransactionsToggle(): EncapsulatedElementType {
return Matchers.getElementByID(AdvancedViewSelectorsIDs.STX_OPT_IN_SWITCH);
}
- get resetAccountButton(): DetoxElement {
+ get resetAccountButton(): EncapsulatedElementType {
return Matchers.getElementByText(
AdvancedViewSelectorsText.RESET_ACCOUNT,
1,
);
}
- get resetConfirmButton(): DetoxElement {
+ get resetConfirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
AdvancedViewSelectorsIDs.RESET_ACCOUNT_CONFIRM_BUTTON,
);
diff --git a/tests/page-objects/Settings/AesCryptoTestForm.ts b/tests/page-objects/Settings/AesCryptoTestForm.ts
index 1449cbc4b94..f3462347905 100644
--- a/tests/page-objects/Settings/AesCryptoTestForm.ts
+++ b/tests/page-objects/Settings/AesCryptoTestForm.ts
@@ -8,6 +8,7 @@ import {
} from '../../../app/components/Views/AesCryptoTestForm/AesCrypto.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class AesCryptoTestForm {
get scrollViewIdentifier(): Promise {
@@ -15,108 +16,108 @@ class AesCryptoTestForm {
}
// Get account address
- get accountAddress(): DetoxElement {
+ get accountAddress(): EncapsulatedElementType {
return Matchers.getElementByID(accountAddress);
}
// Get response text
- get responseText(): DetoxElement {
+ get responseText(): EncapsulatedElementType {
return Matchers.getElementByID(responseText);
}
// Generate salt getters
- get generateSaltBytesCountInput(): DetoxElement {
+ get generateSaltBytesCountInput(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormInputs.saltBytesCountInput);
}
- get generateSaltResponse(): Promise {
+ get generateSaltResponse(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormResponses.saltResponse);
}
- get generateSaltButton(): DetoxElement {
+ get generateSaltButton(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormButtons.generateSaltButton);
}
// Generate encryption key from password getters
- get generateEncryptionKeyPasswordInput(): DetoxElement {
+ get generateEncryptionKeyPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormInputs.passwordInput);
}
- get generateEncryptionKeySaltInput(): DetoxElement {
+ get generateEncryptionKeySaltInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.saltInputForEncryptionKey,
);
}
- get generateEncryptionKeyResponse(): Promise {
+ get generateEncryptionKeyResponse(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormResponses.generateEncryptionKeyResponse,
);
}
- get generateEncryptionKeyButton(): DetoxElement {
+ get generateEncryptionKeyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormButtons.generateEncryptionKeyButton,
);
}
// Encrypt getters
- get encryptDataInput(): DetoxElement {
+ get encryptDataInput(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormInputs.dataInputForEncryption);
}
- get encryptPasswordInput(): DetoxElement {
+ get encryptPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.passwordInputForEncryption,
);
}
- get encryptResponse(): DetoxElement {
+ get encryptResponse(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormResponses.encryptionResponse);
}
- get encryptButton(): DetoxElement {
+ get encryptButton(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormButtons.encryptButton);
}
// Decrypt getters
- get decryptPasswordInput(): DetoxElement {
+ get decryptPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.passwordInputForDecryption,
);
}
- get decryptResponse(): DetoxElement {
+ get decryptResponse(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormResponses.decryptionResponse);
}
- get decryptButton(): DetoxElement {
+ get decryptButton(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormButtons.decryptButton);
}
// Encrypt with key getters
- get encryptWithKeyEncryptionKeyInput(): DetoxElement {
+ get encryptWithKeyEncryptionKeyInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.encryptionKeyInputForEncryptionWithKey,
);
}
- get encryptWithKeyDataInput(): DetoxElement {
+ get encryptWithKeyDataInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.dataInputForEncryptionWithKey,
);
}
- get encryptWithKeyResponse(): DetoxElement {
+ get encryptWithKeyResponse(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormResponses.encryptionWithKeyResponse,
);
}
- get encryptWithKeyButton(): DetoxElement {
+ get encryptWithKeyButton(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormButtons.encryptWithKeyButton);
}
// Decrypt with key getters
- get decryptWithKeyEncryptionKeyInput(): DetoxElement {
+ get decryptWithKeyEncryptionKeyInput(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormInputs.encryptionKeyInputForDecryptionWithKey,
);
}
- get decryptWithKeyResponse(): DetoxElement {
+ get decryptWithKeyResponse(): EncapsulatedElementType {
return Matchers.getElementByID(
aesCryptoFormResponses.decryptionWithKeyResponse,
);
}
- get decryptWithKeyButton(): DetoxElement {
+ get decryptWithKeyButton(): EncapsulatedElementType {
return Matchers.getElementByID(aesCryptoFormButtons.decryptWithKeyButton);
}
@@ -185,7 +186,7 @@ class AesCryptoTestForm {
});
const responseFieldAtts = await (
- await this.generateSaltResponse
+ (await this.generateSaltResponse) as Detox.IndexableNativeElement
).getAttributes();
return (responseFieldAtts as { label: string }).label;
@@ -210,7 +211,7 @@ class AesCryptoTestForm {
});
const responseFieldAtts = await (
- await this.generateEncryptionKeyResponse
+ (await this.generateEncryptionKeyResponse) as Detox.IndexableNativeElement
).getAttributes();
return (responseFieldAtts as { label: string }).label;
diff --git a/tests/page-objects/Settings/BackupAndSyncView.ts b/tests/page-objects/Settings/BackupAndSyncView.ts
index 3196901d4d4..7de45d1fe83 100644
--- a/tests/page-objects/Settings/BackupAndSyncView.ts
+++ b/tests/page-objects/Settings/BackupAndSyncView.ts
@@ -1,21 +1,22 @@
import { BackupAndSyncViewSelectorsIDs } from '../../selectors/Settings/BackupAndSyncView.selectors';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class BackupAndSyncView {
- get accountSyncToggle(): DetoxElement {
+ get accountSyncToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
BackupAndSyncViewSelectorsIDs.ACCOUNT_SYNC_TOGGLE,
);
}
- get backupAndSyncToggle(): DetoxElement {
+ get backupAndSyncToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
BackupAndSyncViewSelectorsIDs.BACKUP_AND_SYNC_TOGGLE,
);
}
- get contactSyncToggle(): DetoxElement {
+ get contactSyncToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
BackupAndSyncViewSelectorsIDs.CONTACTS_SYNC_TOGGLE,
);
diff --git a/tests/page-objects/Settings/Contacts/AddContactView.ts b/tests/page-objects/Settings/Contacts/AddContactView.ts
index ab00d3f9c70..388ce927bcb 100644
--- a/tests/page-objects/Settings/Contacts/AddContactView.ts
+++ b/tests/page-objects/Settings/Contacts/AddContactView.ts
@@ -4,47 +4,48 @@ import {
AddContactViewSelectorsText,
} from '../../../../app/components/Views/Settings/Contacts/AddContactView.testIds';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class AddContactView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(AddContactViewSelectorsIDs.CONTAINER);
}
- get addButton(): DetoxElement {
+ get addButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(AddContactViewSelectorsIDs.ADD_BUTTON)
: Matchers.getElementByLabel(AddContactViewSelectorsIDs.ADD_BUTTON);
}
- get editButton(): DetoxElement {
+ get editButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(AddContactViewSelectorsIDs.EDIT_BUTTON)
: Matchers.getElementByLabel(AddContactViewSelectorsText.EDIT_BUTTON);
}
- get editContact(): DetoxElement {
+ get editContact(): EncapsulatedElementType {
return Matchers.getElementByText(AddContactViewSelectorsText.EDIT_CONTACT);
}
- get deleteButton(): DetoxElement {
+ get deleteButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(AddContactViewSelectorsIDs.DELETE_BUTTON)
: Matchers.getElementByLabel(AddContactViewSelectorsIDs.DELETE_BUTTON);
}
- get nameInput(): DetoxElement {
+ get nameInput(): EncapsulatedElementType {
return Matchers.getElementByID(AddContactViewSelectorsIDs.NAME_INPUT);
}
- get memoLabel(): DetoxElement {
+ get memoLabel(): EncapsulatedElementType {
return Matchers.getElementByText(AddContactViewSelectorsText.MEMO);
}
- get memoInput(): DetoxElement {
+ get memoInput(): EncapsulatedElementType {
return Matchers.getElementByID(AddContactViewSelectorsIDs.MEMO_INPUT);
}
- get addressInput(): DetoxElement {
+ get addressInput(): EncapsulatedElementType {
return Matchers.getElementByID(AddContactViewSelectorsIDs.ADDRESS_INPUT);
}
diff --git a/tests/page-objects/Settings/Contacts/ContactsView.ts b/tests/page-objects/Settings/Contacts/ContactsView.ts
index 7a45772889f..d54189cdbf3 100644
--- a/tests/page-objects/Settings/Contacts/ContactsView.ts
+++ b/tests/page-objects/Settings/Contacts/ContactsView.ts
@@ -2,13 +2,14 @@ import { ContactsViewSelectorIDs } from '../../../../app/components/Views/Settin
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import Assertions from '../../../framework/Assertions';
+import { EncapsulatedElementType } from '../../../framework';
class ContactsView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ContactsViewSelectorIDs.CONTAINER);
}
- get addButton(): DetoxElement {
+ get addButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(ContactsViewSelectorIDs.ADD_BUTTON)
: Matchers.getElementByLabel(ContactsViewSelectorIDs.ADD_BUTTON);
diff --git a/tests/page-objects/Settings/Contacts/DeleteContactBottomSheet.ts b/tests/page-objects/Settings/Contacts/DeleteContactBottomSheet.ts
index e7f0b45c27c..889b00da780 100644
--- a/tests/page-objects/Settings/Contacts/DeleteContactBottomSheet.ts
+++ b/tests/page-objects/Settings/Contacts/DeleteContactBottomSheet.ts
@@ -1,15 +1,16 @@
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import { DeleteContactBottomSheetSelectorsText } from '../../../selectors/Settings/Contacts/DeleteContactBottomSheet.selectors';
+import { EncapsulatedElementType } from '../../../framework';
class DeleteContactBottomSheet {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(
DeleteContactBottomSheetSelectorsText.MODAL_TITLE,
);
}
- get deleteButton(): DetoxElement {
+ get deleteButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByText(
DeleteContactBottomSheetSelectorsText.DELETE_BUTTON,
diff --git a/tests/page-objects/Settings/NetworksView.ts b/tests/page-objects/Settings/NetworksView.ts
index 08fd4c6d8c3..39dcb373bc1 100644
--- a/tests/page-objects/Settings/NetworksView.ts
+++ b/tests/page-objects/Settings/NetworksView.ts
@@ -5,71 +5,72 @@ import {
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { NetworkListModalSelectorsIDs } from '../../../app/components/Views/NetworkSelector/NetworkListModal.testIds';
+import { EncapsulatedElementType } from '../../framework';
class NetworkView {
- get networkContainer(): DetoxElement {
+ get networkContainer(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.NETWORK_CONTAINER);
}
- get networkFormContainer(): DetoxElement {
+ get networkFormContainer(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.CONTAINER);
}
- get rpcContainer(): DetoxElement {
+ get rpcContainer(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.RPC_CONTAINER);
}
- get addNetworkButtonForm(): DetoxElement {
+ get addNetworkButtonForm(): EncapsulatedElementType {
return Matchers.getElementByID(NetworkListModalSelectorsIDs.ADD_BUTTON);
}
- get addRpcDropDownButton(): DetoxElement {
+ get addRpcDropDownButton(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.ICON_BUTTON_RPC);
}
- get addBlockExplorerDropDownButton(): DetoxElement {
+ get addBlockExplorerDropDownButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.ICON_BUTTON_BLOCK_EXPLORER,
);
}
- get addBlockExplorerButton(): DetoxElement {
+ get addBlockExplorerButton(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_BLOCK_EXPLORER);
}
- get addRpcButton(): DetoxElement {
+ get addRpcButton(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_RPC_BUTTON);
}
- get noMatchingText(): DetoxElement {
+ get noMatchingText(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkViewSelectorsText.NO_MATCHING_SEARCH_RESULTS,
);
}
- get emptyPopularNetworksText(): DetoxElement {
+ get emptyPopularNetworksText(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkViewSelectorsText.EMPTY_POPULAR_NETWORKS,
);
}
- get closeIcon(): DetoxElement {
+ get closeIcon(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.CLOSE_ICON);
}
- get deleteNetworkButton(): DetoxElement {
+ get deleteNetworkButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.REMOVE_NETWORK_BUTTON,
);
}
- get networkSearchInput(): DetoxElement {
+ get networkSearchInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.SEARCH_NETWORK_INPUT_BOX_ID,
);
}
- get addNetworkButton(): DetoxElement {
+ get addNetworkButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_NETWORKS_BUTTON)
: Matchers.getElementByLabel(
@@ -77,69 +78,69 @@ class NetworkView {
);
}
- get customNetworkTab(): DetoxElement {
+ get customNetworkTab(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkViewSelectorsText.CUSTOM_NETWORK_TAB,
);
}
- get networkNameInput(): DetoxElement {
+ get networkNameInput(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.NETWORK_NAME_INPUT);
}
- get rpcURLInput(): DetoxElement {
+ get rpcURLInput(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.RPC_URL_INPUT);
}
- get chainIDInput(): DetoxElement {
+ get chainIDInput(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.CHAIN_INPUT);
}
- get networkSymbolInput(): DetoxElement {
+ get networkSymbolInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.NETWORKS_SYMBOL_INPUT,
);
}
- get networkBlockExplorerInput(): DetoxElement {
+ get networkBlockExplorerInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.BLOCK_EXPLORER_INPUT,
);
}
- get rpcAddButton(): DetoxElement {
+ get rpcAddButton(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON,
);
}
- get blockExplorer(): DetoxElement {
+ get blockExplorer(): EncapsulatedElementType {
return Matchers.getElementByLabel(NetworkViewSelectorsText.BLOCK_EXPLORER);
}
- get chainIdLabel(): DetoxElement {
+ get chainIdLabel(): EncapsulatedElementType {
return Matchers.getElementByLabel(NetworkViewSelectorsText.CHAIN_ID_LABEL);
}
- get rpcWarningBanner(): DetoxElement {
+ get rpcWarningBanner(): EncapsulatedElementType {
return Matchers.getElementByID(NetworksViewSelectorsIDs.RPC_WARNING_BANNER);
}
- get customNetworkList(): DetoxElement {
+ get customNetworkList(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworksViewSelectorsIDs.CUSTOM_NETWORK_LIST,
);
}
- get removeNetwork(): DetoxElement {
+ get removeNetwork(): EncapsulatedElementType {
return Matchers.getElementByText(NetworkViewSelectorsText.REMOVE_NETWORK);
}
- get saveButton(): DetoxElement {
+ get saveButton(): EncapsulatedElementType {
return Matchers.getElementByText(NetworkViewSelectorsText.SAVE_BUTTON);
}
- async getnetworkName(networkName: string): Promise {
+ async getnetworkName(networkName: string): Promise {
return Matchers.getElementByText(networkName);
}
async tapAddNetworkButton(): Promise {
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/AutoLockModal.ts b/tests/page-objects/Settings/SecurityAndPrivacy/AutoLockModal.ts
index 511d7bec985..a78c42a17bf 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/AutoLockModal.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/AutoLockModal.ts
@@ -1,9 +1,10 @@
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import { AutoLockModalSelectorsText } from '../../../selectors/Settings/SecurityAndPrivacy/AutoLockModal.selectors';
+import { EncapsulatedElementType } from '../../../framework';
class AutoLockModal {
- get autoLockImmediate(): DetoxElement {
+ get autoLockImmediate(): EncapsulatedElementType {
return Matchers.getElementByText(
AutoLockModalSelectorsText.AUTO_LOCK_IMMEDIATE,
);
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/ChangePasswordView.ts b/tests/page-objects/Settings/SecurityAndPrivacy/ChangePasswordView.ts
index 288761b2b30..99cc3b76000 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/ChangePasswordView.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/ChangePasswordView.ts
@@ -2,33 +2,34 @@ import { ChoosePasswordSelectorsIDs } from '../../../../app/components/Views/Cho
import { ChangePasswordViewSelectorsText } from '../../../selectors/Settings/SecurityAndPrivacy/ChangePasswordView.selectors';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class ChangePasswordView {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(
ChangePasswordViewSelectorsText.CHANGE_PASSWORD,
);
}
- get passwordInput(): DetoxElement {
+ get passwordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
ChoosePasswordSelectorsIDs.NEW_PASSWORD_INPUT_ID,
);
}
- get confirmPasswordInput(): DetoxElement {
+ get confirmPasswordInput(): EncapsulatedElementType {
return Matchers.getElementByID(
ChoosePasswordSelectorsIDs.CONFIRM_PASSWORD_INPUT_ID,
);
}
- get iUnderstandCheckBox(): DetoxElement {
+ get iUnderstandCheckBox(): EncapsulatedElementType {
return Matchers.getElementByLabel(
ChoosePasswordSelectorsIDs.I_UNDERSTAND_CHECKBOX_ID,
);
}
- get submitButton(): DetoxElement {
+ get submitButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ChoosePasswordSelectorsIDs.SAVE_PASSWORD_BUTTON_TEXT,
);
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/ClearPrivacyModal.ts b/tests/page-objects/Settings/SecurityAndPrivacy/ClearPrivacyModal.ts
index aca7a31842d..47fa592513b 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/ClearPrivacyModal.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/ClearPrivacyModal.ts
@@ -4,18 +4,19 @@ import {
} from '../../../../app/components/Views/Settings/SecuritySettings/Sections/ClearPrivacy/ClearPrivacyModal.testIds';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class ClearPrivacyModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ClearPrivacyModalSelectorsIDs.CONTAINER);
}
- get clearButton(): DetoxElement {
+ get clearButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ClearPrivacyModalSelectorsText.CLEAR_BUTTON,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ClearPrivacyModalSelectorsText.CANCEL_BUTTON,
);
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/DeleteWalletModal.ts b/tests/page-objects/Settings/SecurityAndPrivacy/DeleteWalletModal.ts
index 2c2c6b326fc..68e8b1def00 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/DeleteWalletModal.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/DeleteWalletModal.ts
@@ -4,23 +4,24 @@ import {
} from '../../../../app/components/UI/DeleteWalletModal/DeleteWalletModal.testIds';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class DeleteWalletModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(DeleteWalletModalSelectorsIDs.CONTAINER);
}
- get understandButton(): DetoxElement {
+ get understandButton(): EncapsulatedElementType {
return Matchers.getElementByText(
DeleteWalletModalSelectorsText.UNDERSTAND_BUTTON,
);
}
- get deleteWalletButton(): DetoxElement {
+ get deleteWalletButton(): EncapsulatedElementType {
return Matchers.getElementByText(DeleteWalletModalSelectorsText.DELETE_MY);
}
- get deleteInput(): DetoxElement {
+ get deleteInput(): EncapsulatedElementType {
return Matchers.getElementByID(DeleteWalletModalSelectorsIDs.INPUT);
}
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.ts b/tests/page-objects/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.ts
index bed7260f302..2617249c153 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.ts
@@ -5,21 +5,22 @@ import {
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import Utilities from '../../../framework/Utilities';
+import { EncapsulatedElementType } from '../../../framework';
class RevealSecretRecoveryPhrase {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_CONTAINER_ID,
);
}
- get passwordWarning(): DetoxElement {
+ get passwordWarning(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.PASSWORD_WARNING_ID,
);
}
- get passwordInputToRevealCredential(): DetoxElement {
+ get passwordInputToRevealCredential(): EncapsulatedElementType {
return Matchers.getElementByLabel(
RevealSeedViewSelectorsIDs.PASSWORD_INPUT_BOX_ID,
);
@@ -42,37 +43,37 @@ class RevealSecretRecoveryPhrase {
);
}
- get revealSecretRecoveryPhraseButton(): DetoxElement {
+ get revealSecretRecoveryPhraseButton(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_BUTTON_ID,
);
}
- get revealCredentialCopyToClipboardButton(): DetoxElement {
+ get revealCredentialCopyToClipboardButton(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_COPY_TO_CLIPBOARD_BUTTON,
);
}
- get revealCredentialQRCodeTab(): DetoxElement {
+ get revealCredentialQRCodeTab(): EncapsulatedElementType {
return Matchers.getElementByText(
RevealSeedViewSelectorsText.REVEAL_CREDENTIAL_QR_CODE_TAB_ID,
);
}
- get revealCredentialQRCodeImage(): DetoxElement {
+ get revealCredentialQRCodeImage(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.REVEAL_CREDENTIAL_QR_CODE_IMAGE_ID,
);
}
- get doneButton(): DetoxElement {
+ get doneButton(): EncapsulatedElementType {
return Matchers.getElementByText(
RevealSeedViewSelectorsText.REVEAL_CREDENTIAL_DONE,
);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
RevealSeedViewSelectorsIDs.SECRET_RECOVERY_PHRASE_NEXT_BUTTON_ID,
);
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.ts b/tests/page-objects/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.ts
index d6eb0a01aa7..c6a3053c657 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.ts
@@ -4,32 +4,33 @@ import {
} from '../../../../app/components/Views/Settings/SecuritySettings/SecurityPrivacyView.testIds';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class SecurityAndPrivacy {
- get changePasswordButton(): DetoxElement {
+ get changePasswordButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.CHANGE_PASSWORD_BUTTON,
);
}
- get revealSecretRecoveryPhraseButton(): DetoxElement {
+ get revealSecretRecoveryPhraseButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.REVEAL_SEED_BUTTON,
);
}
- get clearPrivacyDataButton(): DetoxElement {
+ get clearPrivacyDataButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.CLEAR_PRIVACY_DATA_BUTTON,
);
}
- get securityAndPrivacyHeading(): DetoxElement {
+ get securityAndPrivacyHeading(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.SECURITY_AND_PRIVACY_HEADING,
);
}
- get deleteWalletButton(): DetoxElement {
+ get deleteWalletButton(): EncapsulatedElementType {
return device.getPlatform() === 'ios'
? Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.DELETE_WALLET_BUTTON,
@@ -39,7 +40,7 @@ class SecurityAndPrivacy {
);
}
- get metaMetricsToggle(): DetoxElement {
+ get metaMetricsToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.METAMETRICS_SWITCH,
);
@@ -50,60 +51,60 @@ class SecurityAndPrivacy {
);
}
- get autoLockSection(): DetoxElement {
+ get autoLockSection(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.AUTO_LOCK_SECTION,
);
}
- get autoLockDefault30Seconds(): DetoxElement {
+ get autoLockDefault30Seconds(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.AUTO_LOCK_30_SECONDS,
);
}
- get rememberMeToggle(): DetoxElement {
+ get rememberMeToggle(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.REMEMBER_ME_TOGGLE,
);
}
- get changePasswordSection(): DetoxElement {
+ get changePasswordSection(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.CHANGE_PASSWORD_CONTAINER,
);
}
- get securitySettingsScroll(): DetoxElement {
+ get securitySettingsScroll(): EncapsulatedElementType {
return Matchers.getElementByID(
SecurityPrivacyViewSelectorsIDs.SECURITY_SETTINGS_SCROLL,
);
}
- get showPrivateKey(): DetoxElement {
+ get showPrivateKey(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.SHOW_PRIVATE_KEY,
);
}
- get showPrivateKeyButton(): DetoxElement {
+ get showPrivateKeyButton(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.SHOW_PRIVATE_KEY,
);
}
- get backUpNow(): DetoxElement {
+ get backUpNow(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.BACK_UP_NOW,
);
}
- get privacyHeader(): DetoxElement {
+ get privacyHeader(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.PRIVACY_HEADING,
);
}
- get clearBrowserCookiesButton(): DetoxElement {
+ get clearBrowserCookiesButton(): EncapsulatedElementType {
return Matchers.getElementByText(
SecurityPrivacyViewSelectorsText.CLEAR_BROWSER_COOKIES,
);
diff --git a/tests/page-objects/Settings/SecurityAndPrivacy/SrpQuizModal.ts b/tests/page-objects/Settings/SecurityAndPrivacy/SrpQuizModal.ts
index 01da2edd8fa..94eb70032b8 100644
--- a/tests/page-objects/Settings/SecurityAndPrivacy/SrpQuizModal.ts
+++ b/tests/page-objects/Settings/SecurityAndPrivacy/SrpQuizModal.ts
@@ -8,24 +8,25 @@ import {
} from '../../../../app/components/Views/Quiz/SRPQuiz/SrpQuizModal.testIds';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class SrpQuizModal {
// Getters for common elements
- get getStartedContainer(): DetoxElement {
+ get getStartedContainer(): EncapsulatedElementType {
return Matchers.getElementByID(SrpQuizGetStartedSelectorsIDs.CONTAINER);
}
- get getStartedScreenDismiss(): DetoxElement {
+ get getStartedScreenDismiss(): EncapsulatedElementType {
return Matchers.getElementByID(SrpQuizGetStartedSelectorsIDs.DISMISS);
}
- get modalIntroduction(): DetoxElement {
+ get modalIntroduction(): EncapsulatedElementType {
return Matchers.getElementByText(
SrpQuizGetStartedSelectorsText.INTRODUCTION,
);
}
- get getStartedButton(): DetoxElement {
+ get getStartedButton(): EncapsulatedElementType {
return Matchers.getElementByID(SrpQuizGetStartedSelectorsIDs.BUTTON);
}
diff --git a/tests/page-objects/Settings/SettingsView.ts b/tests/page-objects/Settings/SettingsView.ts
index 4dbcfee4333..3ff60132d34 100644
--- a/tests/page-objects/Settings/SettingsView.ts
+++ b/tests/page-objects/Settings/SettingsView.ts
@@ -5,60 +5,61 @@ import {
SettingsViewSelectorsText,
} from '../../../app/components/Views/Settings/SettingsView.testIds';
import { CommonSelectorsText } from '../../../app/util/Common.testIds';
+import { EncapsulatedElementType } from '../../framework';
class SettingsView {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(SettingsViewSelectorsText.TITLE);
}
- get generalSettingsButton(): DetoxElement {
+ get generalSettingsButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.GENERAL);
}
- get advancedButton(): DetoxElement {
+ get advancedButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.ADVANCED);
}
- get contactsSettingsButton(): DetoxElement {
+ get contactsSettingsButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.CONTACTS);
}
- get securityAndPrivacyButton(): DetoxElement {
+ get securityAndPrivacyButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.SECURITY);
}
- get notificationsButton(): DetoxElement {
+ get notificationsButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.NOTIFICATIONS);
}
- get aesCryptoTestForm(): DetoxElement {
+ get aesCryptoTestForm(): EncapsulatedElementType {
return Matchers.getElementByID(
SettingsViewSelectorsIDs.AES_CRYPTO_TEST_FORM,
);
}
- get lockSettingsButton(): DetoxElement {
+ get lockSettingsButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.LOCK);
}
- get contactSupportButton(): DetoxElement {
+ get contactSupportButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.CONTACT);
}
- get contactSupportSectionTitle(): DetoxElement {
+ get contactSupportSectionTitle(): EncapsulatedElementType {
return Matchers.getElementByText(
SettingsViewSelectorsText.CONTACT_SUPPORT_TITLE,
);
}
- get backupAndSyncSectionButton(): DetoxElement {
+ get backupAndSyncSectionButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.BACKUP_AND_SYNC);
}
- get snapsSectionButton(): DetoxElement {
+ get snapsSectionButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.SNAPS);
}
- get alertButton(): DetoxElement {
+ get alertButton(): EncapsulatedElementType {
return device.getPlatform() === 'android'
? Matchers.getElementByText(CommonSelectorsText.YES_ALERT_BUTTON)
: Matchers.getElementByLabel(CommonSelectorsText.YES_ALERT_BUTTON);
@@ -194,7 +195,7 @@ class SettingsView {
});
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(SettingsViewSelectorsIDs.BACK_BUTTON);
}
diff --git a/tests/page-objects/Settings/SnapSettingsView.ts b/tests/page-objects/Settings/SnapSettingsView.ts
index e5e9c28aa0c..0a3376484be 100644
--- a/tests/page-objects/Settings/SnapSettingsView.ts
+++ b/tests/page-objects/Settings/SnapSettingsView.ts
@@ -1,12 +1,13 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class SnapSettingsView {
- get enabledToggle(): DetoxElement {
+ get enabledToggle(): EncapsulatedElementType {
return Matchers.getElementByID('snap-details-switch');
}
- get removeButton(): DetoxElement {
+ get removeButton(): EncapsulatedElementType {
return Matchers.getElementByID('snap-settings-remove-button');
}
@@ -14,11 +15,11 @@ class SnapSettingsView {
return Matchers.getIdentifier('snap-settings-scrollview');
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID('snap-settings-back-button');
}
- get listBackButton(): DetoxElement {
+ get listBackButton(): EncapsulatedElementType {
return Matchers.getElementByID('snaps-settings-list-back-button');
}
diff --git a/tests/page-objects/Stake/StakeConfirmView.ts b/tests/page-objects/Stake/StakeConfirmView.ts
index 524abdc493f..46f0ed67851 100644
--- a/tests/page-objects/Stake/StakeConfirmView.ts
+++ b/tests/page-objects/Stake/StakeConfirmView.ts
@@ -1,9 +1,10 @@
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import { StakeConfirmViewSelectors } from '../../selectors/Stake/StakeConfirmView.selectors.js';
+import { EncapsulatedElementType } from '../../framework';
class StakeConfirmationView {
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByText(StakeConfirmViewSelectors.CONFIRM);
}
diff --git a/tests/page-objects/Stake/StakeView.ts b/tests/page-objects/Stake/StakeView.ts
index 269ebc50098..19966d07ee0 100644
--- a/tests/page-objects/Stake/StakeView.ts
+++ b/tests/page-objects/Stake/StakeView.ts
@@ -2,21 +2,22 @@ import { StakeViewSelectors } from '../../selectors/Stake/StakeView.selectors.js
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import Utilities from '../../framework/Utilities';
+import { EncapsulatedElementType } from '../../framework';
class StakeView {
- get stakeContainer(): DetoxElement {
+ get stakeContainer(): EncapsulatedElementType {
return Matchers.getElementByText(StakeViewSelectors.STAKE_CONTAINER);
}
- get unstakeContainer(): DetoxElement {
+ get unstakeContainer(): EncapsulatedElementType {
return Matchers.getElementByText(StakeViewSelectors.UNSTAKE_CONTAINER);
}
- get reviewButton(): DetoxElement {
+ get reviewButton(): EncapsulatedElementType {
return Matchers.getElementByText(StakeViewSelectors.REVIEW_BUTTON);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByText(StakeViewSelectors.CONFIRM);
}
diff --git a/tests/page-objects/Transactions/ActivitiesView.ts b/tests/page-objects/Transactions/ActivitiesView.ts
index 0d79b3d30bb..73dadd6942e 100644
--- a/tests/page-objects/Transactions/ActivitiesView.ts
+++ b/tests/page-objects/Transactions/ActivitiesView.ts
@@ -12,88 +12,89 @@ import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import Assertions from '../../framework/Assertions';
import { encapsulatedAction } from '../../framework/encapsulatedAction';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import PlaywrightAssertions from '../../framework/PlaywrightAssertions';
class ActivitiesView {
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.TITLE);
}
- get predictionsTab(): DetoxElement {
+ get predictionsTab(): EncapsulatedElementType {
return Matchers.getElementByLabel(
ActivitiesViewSelectorsText.PREDICTIONS_TAB,
);
}
- get transferTab(): DetoxElement {
+ get transferTab(): EncapsulatedElementType {
return Matchers.getElementByID(ActivitiesViewSelectorsIDs.TRANSFER_TAB);
}
- get tabsBar(): DetoxElement {
+ get tabsBar(): EncapsulatedElementType {
return Matchers.getElementByID(
`${ActivitiesViewSelectorsIDs.TABS_CONTAINER}-bar`,
);
}
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ActivitiesViewSelectorsIDs.CONTAINER);
}
- get confirmedLabel(): DetoxElement {
+ get confirmedLabel(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.CONFIRM_TEXT);
}
- get stakeDepositedLabel(): DetoxElement {
+ get stakeDepositedLabel(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.STAKE_DEPOSIT);
}
- get stakeMoreDepositedLabel(): DetoxElement {
+ get stakeMoreDepositedLabel(): EncapsulatedElementType {
return Matchers.getElementByText(
ActivitiesViewSelectorsText.STAKE_DEPOSIT,
0,
);
}
- get unstakeLabel(): DetoxElement {
+ get unstakeLabel(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.UNSTAKE);
}
- get stackingClaimLabel(): DetoxElement {
+ get stackingClaimLabel(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.STAKING_CLAIM);
}
- get approveActivity(): DetoxElement {
+ get approveActivity(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.APPROVE);
}
- get lendingDepositActivity(): DetoxElement {
+ get lendingDepositActivity(): EncapsulatedElementType {
return Matchers.getElementByText(
ActivitiesViewSelectorsText.LENDING_DEPOSIT,
);
}
- get lendingWithdrawalActivity(): DetoxElement {
+ get lendingWithdrawalActivity(): EncapsulatedElementType {
return Matchers.getElementByText(
ActivitiesViewSelectorsText.LENDING_WITHDRAWAL,
);
}
- get predictDeposit(): DetoxElement {
+ get predictDeposit(): EncapsulatedElementType {
return Matchers.getElementByText(
ActivitiesViewSelectorsText.PREDICT_DEPOSIT,
);
}
- get predictWithdraw(): DetoxElement {
+ get predictWithdraw(): EncapsulatedElementType {
return Matchers.getElementByText(
ActivitiesViewSelectorsText.PREDICT_WITHDRAW,
);
}
- transactionStatus(row: number): DetoxElement {
+ transactionStatus(row: number): EncapsulatedElementType {
return Matchers.getElementByID(`transaction-status-${row}`);
}
- transactionItem(row: number): DetoxElement {
+ transactionItem(row: number): EncapsulatedElementType {
return Matchers.getElementByID(`transaction-item-${row}`);
}
@@ -116,17 +117,17 @@ class ActivitiesView {
swapActivityTitle(
sourceToken: string,
destinationToken: string,
- ): DetoxElement {
+ ): EncapsulatedElementType {
return Matchers.getElementByText(
this.generateSwapActivityLabel(sourceToken, destinationToken),
);
}
- swapApprovalActivityTitle(): DetoxElement {
+ swapApprovalActivityTitle(): EncapsulatedElementType {
return Matchers.getElementByText(ActivitiesViewSelectorsText.APPROVE);
}
- bridgeActivityTitle(destNetwork: string): DetoxElement {
+ bridgeActivityTitle(destNetwork: string): EncapsulatedElementType {
return Matchers.getElementByText(
this.generateBridgeActivityLabel(destNetwork),
);
@@ -184,7 +185,7 @@ class ActivitiesView {
rampsOrderCryptoAmount(
orderType: RampsOrderTypeSlug,
rowIndex: number,
- ): DetoxElement {
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
getOrderRowCryptoAmountTestId(orderType, rowIndex),
);
@@ -193,7 +194,7 @@ class ActivitiesView {
rampsOrderFiatAmount(
orderType: RampsOrderTypeSlug,
rowIndex: number,
- ): DetoxElement {
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
getOrderRowFiatAmountTestId(orderType, rowIndex),
);
diff --git a/tests/page-objects/Transactions/AssetWatchBottomSheet.ts b/tests/page-objects/Transactions/AssetWatchBottomSheet.ts
index 0e437df240f..bc59fb9b34e 100644
--- a/tests/page-objects/Transactions/AssetWatchBottomSheet.ts
+++ b/tests/page-objects/Transactions/AssetWatchBottomSheet.ts
@@ -1,16 +1,17 @@
import { AssetWatcherSelectorsIDs } from '../../../app/components/Views/confirmations/legacy/components/WatchAssetRequest/AssetWatcher.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class AssetWatchBottomSheet {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(AssetWatcherSelectorsIDs.CONTAINER);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(AssetWatcherSelectorsIDs.CANCEL_BUTTON);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(AssetWatcherSelectorsIDs.CONFIRM_BUTTON);
}
diff --git a/tests/page-objects/Transactions/TransactionDetailsModal.ts b/tests/page-objects/Transactions/TransactionDetailsModal.ts
index fd245259e4f..2863f90f33a 100644
--- a/tests/page-objects/Transactions/TransactionDetailsModal.ts
+++ b/tests/page-objects/Transactions/TransactionDetailsModal.ts
@@ -5,38 +5,38 @@ import {
} from '../../../app/components/Views/confirmations/components/activity/TransactionDetailsModal.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
-import { Assertions, logger } from '../../framework';
+import { Assertions, logger, EncapsulatedElementType } from '../../framework';
class TransactionDetailsModal {
- get closeIcon(): DetoxElement {
+ get closeIcon(): EncapsulatedElementType {
return Matchers.getElementByID(
TransactionDetailsModalSelectorsIDs.CLOSE_ICON,
);
}
- get networkFee(): DetoxElement {
+ get networkFee(): EncapsulatedElementType {
return Matchers.getElementByID(TransactionDetailsSelectorIDs.NETWORK_FEE);
}
- get paidWithSymbol(): DetoxElement {
+ get paidWithSymbol(): EncapsulatedElementType {
return Matchers.getElementByID(
TransactionDetailsSelectorIDs.PAID_WITH_SYMBOL,
);
}
- get status(): DetoxElement {
+ get status(): EncapsulatedElementType {
return Matchers.getElementByID(TransactionDetailsSelectorIDs.STATUS);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByID(TransactionDetailsModalSelectorsIDs.TITLE);
}
- get total(): DetoxElement {
+ get total(): EncapsulatedElementType {
return Matchers.getElementByID(TransactionDetailsSelectorIDs.TOTAL);
}
- get transactionFee(): DetoxElement {
+ get transactionFee(): EncapsulatedElementType {
return Matchers.getElementByID(
TransactionDetailsSelectorIDs.TRANSACTION_FEE,
);
diff --git a/tests/page-objects/Transactions/UnifiedTransactionsView.ts b/tests/page-objects/Transactions/UnifiedTransactionsView.ts
index 36d8a31ce44..1f30458dd7b 100644
--- a/tests/page-objects/Transactions/UnifiedTransactionsView.ts
+++ b/tests/page-objects/Transactions/UnifiedTransactionsView.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import { UnifiedTransactionsViewSelectorsIDs } from '../../../app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.testIds';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class UnifiedTransactionsView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
UnifiedTransactionsViewSelectorsIDs.CONTAINER,
);
diff --git a/tests/page-objects/Transactions/predictionsActivityDetails.ts b/tests/page-objects/Transactions/predictionsActivityDetails.ts
index 532b4af0a59..41e47489d3e 100644
--- a/tests/page-objects/Transactions/predictionsActivityDetails.ts
+++ b/tests/page-objects/Transactions/predictionsActivityDetails.ts
@@ -1,21 +1,22 @@
import { PredictActivityDetailsSelectorsIDs } from '../../../app/components/UI/Predict/Predict.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class PredictActivityDetails {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictActivityDetailsSelectorsIDs.CONTAINER,
);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictActivityDetailsSelectorsIDs.BACK_BUTTON,
);
}
- get amountDisplay(): DetoxElement {
+ get amountDisplay(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictActivityDetailsSelectorsIDs.AMOUNT_DISPLAY,
);
diff --git a/tests/page-objects/Trending/TrendingView.ts b/tests/page-objects/Trending/TrendingView.ts
index 48877e9a798..7bc8dd2e4f1 100644
--- a/tests/page-objects/Trending/TrendingView.ts
+++ b/tests/page-objects/Trending/TrendingView.ts
@@ -3,6 +3,7 @@ import {
Gestures,
Assertions,
type ScrollOptions,
+ EncapsulatedElementType,
} from '../../framework';
import {
TrendingViewSelectorsIDs,
@@ -18,80 +19,80 @@ import BrowserView from '../Browser/BrowserView';
class TrendingView {
private activeScrollViewID: string = TrendingViewSelectorsIDs.NOW_SCROLL_VIEW;
- get searchButton(): DetoxElement {
+ get searchButton(): EncapsulatedElementType {
return Matchers.getElementByID(TrendingViewSelectorsIDs.SEARCH_BUTTON);
}
- get browserButton(): DetoxElement {
+ get browserButton(): EncapsulatedElementType {
return Matchers.getElementByID(TrendingViewSelectorsIDs.BROWSER_BUTTON);
}
- get searchInputContainer(): DetoxElement {
+ get searchInputContainer(): EncapsulatedElementType {
return Matchers.getElementByID(TrendingViewSelectorsIDs.SEARCH_INPUT);
}
- get searchInput(): DetoxElement {
+ get searchInput(): EncapsulatedElementType {
return Matchers.getElementByID(TrendingViewSelectorsIDs.SEARCH_TEXT_INPUT);
}
- get searchCancelButton(): DetoxElement {
+ get searchCancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(
TrendingViewSelectorsIDs.SEARCH_CANCEL_BUTTON,
);
}
- get searchAllPill(): DetoxElement {
+ get searchAllPill(): EncapsulatedElementType {
return Matchers.getElementByID(TrendingViewSelectorsIDs.SEARCH_PILL_ALL);
}
- get searchCryptosPill(): DetoxElement {
+ get searchCryptosPill(): EncapsulatedElementType {
return Matchers.getElementByID(
TrendingViewSelectorsIDs.SEARCH_PILL_CRYPTOS,
);
}
- get searchResultsList(): DetoxElement {
+ get searchResultsList(): EncapsulatedElementType {
return Matchers.getElementByID(
TrendingViewSelectorsIDs.SEARCH_RESULTS_LIST,
);
}
- getTokenRow(assetId: string): DetoxElement {
+ getTokenRow(assetId: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${TrendingViewSelectorsIDs.TOKEN_ROW_ITEM_PREFIX}${assetId}`,
0,
);
}
- getPerpRow(symbol: string): DetoxElement {
+ getPerpRow(symbol: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${TrendingViewSelectorsIDs.PERPS_ROW_ITEM_PREFIX}${symbol}`,
0,
);
}
- getPredictionRow(id: string): DetoxElement {
+ getPredictionRow(id: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${TrendingViewSelectorsIDs.PREDICTIONS_ROW_ITEM_PREFIX}${id}`,
0,
);
}
- getSiteRow(name: string): DetoxElement {
+ getSiteRow(name: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${TrendingViewSelectorsIDs.SITE_ROW_ITEM_PREFIX}${name}`,
0,
);
}
- getSectionHeader(title: string): DetoxElement {
+ getSectionHeader(title: string): EncapsulatedElementType {
return Matchers.getElementByText(title);
}
/**
* Get section header by testID (for full view headers)
*/
- getSectionHeaderByTestID(title: string): DetoxElement | null {
+ getSectionHeaderByTestID(title: string): EncapsulatedElementType | null {
const headerTestID = SECTION_FULL_VIEW_HEADERS[title];
if (!headerTestID) {
return null;
@@ -99,7 +100,7 @@ class TrendingView {
return Matchers.getElementByID(headerTestID);
}
- getBackButton(testID: string): DetoxElement {
+ getBackButton(testID: string): EncapsulatedElementType {
return Matchers.getElementByID(testID);
}
@@ -166,7 +167,7 @@ class TrendingView {
* Uses Gestures.scrollToElement which retries scroll + visibility check until the element is on screen.
*/
private async scrollToElementInFeed(
- targetElement: DetoxElement,
+ targetElement: EncapsulatedElementType,
description: string,
direction: 'up' | 'down' = 'down',
options: Partial = {},
@@ -285,7 +286,7 @@ class TrendingView {
* @param itemType - Type of item for description ('token', 'perp', 'prediction', 'site')
*/
private async verifyItemVisible(
- getElement: () => DetoxElement,
+ getElement: () => EncapsulatedElementType,
identifier: string,
itemType: string,
options: Partial = {},
@@ -309,7 +310,7 @@ class TrendingView {
* Gestures.scrollToElement retries scroll until the element is visible.
*/
private async tapItemRow(
- getElement: () => DetoxElement,
+ getElement: () => EncapsulatedElementType,
identifier: string,
itemType: string,
): Promise {
diff --git a/tests/page-objects/UI/FundActionMenu.ts b/tests/page-objects/UI/FundActionMenu.ts
index b6217abd73d..cb99ac1fbc8 100644
--- a/tests/page-objects/UI/FundActionMenu.ts
+++ b/tests/page-objects/UI/FundActionMenu.ts
@@ -1,27 +1,28 @@
import { WalletActionsBottomSheetSelectorsIDs } from '../../../app/components/Views/WalletActions/WalletActionsBottomSheet.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class FundActionMenu {
- get depositButton(): DetoxElement {
+ get depositButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.DEPOSIT_BUTTON,
);
}
- get buyButton(): DetoxElement {
+ get buyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON,
);
}
- get unifiedBuyButton(): DetoxElement {
+ get unifiedBuyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.BUY_UNIFIED_BUTTON,
);
}
- get sellButton(): DetoxElement {
+ get sellButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.SELL_BUTTON,
);
diff --git a/tests/page-objects/importAccount/ImportAccountView.ts b/tests/page-objects/importAccount/ImportAccountView.ts
index b1de4140dbe..5f9e0bb1851 100644
--- a/tests/page-objects/importAccount/ImportAccountView.ts
+++ b/tests/page-objects/importAccount/ImportAccountView.ts
@@ -1,19 +1,20 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import { ImportAccountFromPrivateKeyIDs } from '../../../app/components/Views/ImportPrivateKey/ImportAccountFromPrivateKey.testIds';
class ImportAccountView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ImportAccountFromPrivateKeyIDs.CONTAINER);
}
- get importButton(): DetoxElement {
+ get importButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportAccountFromPrivateKeyIDs.IMPORT_BUTTON,
);
}
- get privateKeyField(): DetoxElement {
+ get privateKeyField(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportAccountFromPrivateKeyIDs.PRIVATE_KEY_INPUT_BOX,
);
diff --git a/tests/page-objects/importAccount/SuccessImportAccountView.ts b/tests/page-objects/importAccount/SuccessImportAccountView.ts
index a1ac25e5031..a06fa7015ab 100644
--- a/tests/page-objects/importAccount/SuccessImportAccountView.ts
+++ b/tests/page-objects/importAccount/SuccessImportAccountView.ts
@@ -2,14 +2,18 @@ import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { SuccessImportAccountIDs } from '../../../app/components/Views/ImportPrivateKeySuccess/SuccessImportAccount.testIds';
import WalletView from '../wallet/WalletView';
-import { asDetoxElement, Utilities } from '../../framework';
+import {
+ asDetoxElement,
+ Utilities,
+ EncapsulatedElementType,
+} from '../../framework';
class SuccessImportAccountView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(SuccessImportAccountIDs.CONTAINER);
}
- get closeButton(): DetoxElement {
+ get closeButton(): EncapsulatedElementType {
return Matchers.getElementByID(SuccessImportAccountIDs.CLOSE_BUTTON);
}
diff --git a/tests/page-objects/importSrp/ImportSrpView.ts b/tests/page-objects/importSrp/ImportSrpView.ts
index 8c1d85ebff6..1695e45f80b 100644
--- a/tests/page-objects/importSrp/ImportSrpView.ts
+++ b/tests/page-objects/importSrp/ImportSrpView.ts
@@ -1,25 +1,26 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework/EncapsulatedElement';
import { ImportSRPIDs } from '../../../app/components/Views/ImportNewSecretRecoveryPhrase/SRPImport.testIds';
class ImportSrpView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ImportSRPIDs.CONTAINER);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByID(ImportSRPIDs.SCREEN_TITLE_ID);
}
- get importButton(): DetoxElement {
+ get importButton(): EncapsulatedElementType {
return Matchers.getElementByID(ImportSRPIDs.IMPORT_BUTTON);
}
- get textareaInput(): DetoxElement {
+ get textareaInput(): EncapsulatedElementType {
return Matchers.getElementByID(ImportSRPIDs.SEED_PHRASE_INPUT_ID);
}
- seedPhraseInput(index: number): DetoxElement {
+ seedPhraseInput(index: number): EncapsulatedElementType {
if (index !== 0) {
return Matchers.getElementByID(
`${ImportSRPIDs.SEED_PHRASE_INPUT_ID}_${index}`,
diff --git a/tests/page-objects/swaps/Deeplink.ts b/tests/page-objects/swaps/Deeplink.ts
index 49c16279ae1..fec1533bf12 100644
--- a/tests/page-objects/swaps/Deeplink.ts
+++ b/tests/page-objects/swaps/Deeplink.ts
@@ -1,12 +1,13 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class DeeplinkModal {
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByText('Continue');
}
- get proceedWithCaution(): DetoxElement {
+ get proceedWithCaution(): EncapsulatedElementType {
return Matchers.getElementByText('Proceed with caution');
}
diff --git a/tests/page-objects/swaps/OnBoarding.ts b/tests/page-objects/swaps/OnBoarding.ts
index 05a1981e242..01e3f2dc430 100644
--- a/tests/page-objects/swaps/OnBoarding.ts
+++ b/tests/page-objects/swaps/OnBoarding.ts
@@ -1,9 +1,10 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { OnBoardingSelectors } from '../../selectors/swaps/OnBoarding.selectors';
+import { EncapsulatedElementType } from '../../framework';
class Onboarding {
- get startSwappingButton(): DetoxElement {
+ get startSwappingButton(): EncapsulatedElementType {
return Matchers.getElementByText(OnBoardingSelectors.START_SWAPPING);
}
diff --git a/tests/page-objects/swaps/QuoteView.ts b/tests/page-objects/swaps/QuoteView.ts
index a7120810c3a..da417029875 100644
--- a/tests/page-objects/swaps/QuoteView.ts
+++ b/tests/page-objects/swaps/QuoteView.ts
@@ -32,19 +32,19 @@ const TIMEOUT = {
} as const;
class QuoteView {
- get selectAmountLabel(): DetoxElement {
+ get selectAmountLabel(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.SELECT_AMOUNT);
}
- get confirmBridge(): DetoxElement {
+ get confirmBridge(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteViewSelectorIDs.CONFIRM_BUTTON);
}
- get confirmSwap(): DetoxElement {
+ get confirmSwap(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteViewSelectorIDs.CONFIRM_BUTTON);
}
- get sourceTokenArea(): DetoxElement {
+ get sourceTokenArea(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteViewSelectorIDs.SOURCE_TOKEN_AREA);
}
@@ -96,15 +96,15 @@ class QuoteView {
});
}
- get seeAllButton(): DetoxElement {
+ get seeAllButton(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.SELECT_ALL);
}
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(QuoteViewSelectorIDs.BACK_BUTTON);
}
- get networkFeeLabel(): DetoxElement {
+ get networkFeeLabel(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.NETWORK_FEE);
}
@@ -149,15 +149,15 @@ class QuoteView {
});
}
- get maxLink(): DetoxElement {
+ get maxLink(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.MAX);
}
- get includedLabel(): DetoxElement {
+ get includedLabel(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.INCLUDED);
}
- get rateLabel(): DetoxElement {
+ get rateLabel(): EncapsulatedElementType {
return Matchers.getElementByText(QuoteViewSelectorText.RATE);
}
@@ -509,7 +509,7 @@ class QuoteView {
* Gets the slippage display text element (e.g., "2.5%")
* @param value - The slippage value to match (e.g., "2.5" for 2.5%)
*/
- slippageDisplayText(value: string): DetoxElement {
+ slippageDisplayText(value: string): EncapsulatedElementType {
return Matchers.getElementByText(`${value}%`);
}
diff --git a/tests/page-objects/swaps/SlippageModal.ts b/tests/page-objects/swaps/SlippageModal.ts
index 28f1f37dc62..538d4dc51ee 100644
--- a/tests/page-objects/swaps/SlippageModal.ts
+++ b/tests/page-objects/swaps/SlippageModal.ts
@@ -5,29 +5,30 @@ import {
SlippageModalSelectorIDs,
SlippageModalSelectorText,
} from '../../selectors/Bridge/SlippageModal.selectors';
+import { EncapsulatedElementType } from '../../framework';
class SlippageModal {
- get editSlippageButton(): DetoxElement {
+ get editSlippageButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SlippageModalSelectorIDs.EDIT_SLIPPAGE_BUTTON,
);
}
- get customButton(): DetoxElement {
+ get customButton(): EncapsulatedElementType {
return Matchers.getElementByText(SlippageModalSelectorText.CUSTOM);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByText(SlippageModalSelectorText.CONFIRM);
}
- get inputStepperInput(): DetoxElement {
+ get inputStepperInput(): EncapsulatedElementType {
return Matchers.getElementByID(
SlippageModalSelectorIDs.INPUT_STEPPER_INPUT,
);
}
- get keypadDeleteButton(): DetoxElement {
+ get keypadDeleteButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SlippageModalSelectorIDs.KEYPAD_DELETE_BUTTON,
);
diff --git a/tests/page-objects/swaps/SwapTrendingTokensView.ts b/tests/page-objects/swaps/SwapTrendingTokensView.ts
index 3e29c07fe7c..132f06ea674 100644
--- a/tests/page-objects/swaps/SwapTrendingTokensView.ts
+++ b/tests/page-objects/swaps/SwapTrendingTokensView.ts
@@ -1,4 +1,9 @@
-import { Assertions, Gestures, Matchers } from '../../framework';
+import {
+ Assertions,
+ Gestures,
+ Matchers,
+ EncapsulatedElementType,
+} from '../../framework';
import { BridgeViewSelectorsIDs } from '../../../app/components/UI/Bridge/Views/BridgeView/BridgeView.testIds';
import { BridgeTrendingTokensSectionTestIds } from '../../../app/components/UI/Bridge/components/BridgeTrendingTokensSection/BridgeTrendingTokensSection.testIds';
@@ -13,55 +18,55 @@ const SwapTrendingTokensViewTestIds = {
} as const;
class SwapTrendingTokensView {
- public get section(): DetoxElement {
+ public get section(): EncapsulatedElementType {
return Matchers.getElementByID(BridgeTrendingTokensSectionTestIds.SECTION);
}
- public get priceFilter(): DetoxElement {
+ public get priceFilter(): EncapsulatedElementType {
return Matchers.getElementByID(
BridgeTrendingTokensSectionTestIds.PRICE_FILTER,
);
}
- public get networkFilter(): DetoxElement {
+ public get networkFilter(): EncapsulatedElementType {
return Matchers.getElementByID(
BridgeTrendingTokensSectionTestIds.NETWORK_FILTER,
);
}
- public get timeFilter(): DetoxElement {
+ public get timeFilter(): EncapsulatedElementType {
return Matchers.getElementByID(
BridgeTrendingTokensSectionTestIds.TIME_FILTER,
);
}
- public get priceBottomSheet(): DetoxElement {
+ public get priceBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(
SwapTrendingTokensViewTestIds.PRICE_BOTTOM_SHEET,
);
}
- public get networkBottomSheet(): DetoxElement {
+ public get networkBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(
SwapTrendingTokensViewTestIds.NETWORK_BOTTOM_SHEET,
);
}
- public get timeBottomSheet(): DetoxElement {
+ public get timeBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(
SwapTrendingTokensViewTestIds.TIME_BOTTOM_SHEET,
);
}
- public get innerList(): DetoxElement {
+ public get innerList(): EncapsulatedElementType {
return Matchers.getElementByID(SwapTrendingTokensViewTestIds.INNER_LIST);
}
- public get closeButton(): DetoxElement {
+ public get closeButton(): EncapsulatedElementType {
return Matchers.getElementByID(SwapTrendingTokensViewTestIds.CLOSE_BUTTON);
}
- public get timeSelectSixHours(): DetoxElement {
+ public get timeSelectSixHours(): EncapsulatedElementType {
return Matchers.getElementByID(
SwapTrendingTokensViewTestIds.TIME_SELECT_6H,
);
@@ -106,7 +111,7 @@ class SwapTrendingTokensView {
});
}
- tokenRow(assetId: string): DetoxElement {
+ tokenRow(assetId: string): EncapsulatedElementType {
return Matchers.getElementByID(
`${SwapTrendingTokensViewTestIds.TOKEN_ROW_PREFIX}${assetId}`,
);
diff --git a/tests/page-objects/wallet/AccountActionsBottomSheet.ts b/tests/page-objects/wallet/AccountActionsBottomSheet.ts
index f1bb9a30ca3..b12a131ea66 100644
--- a/tests/page-objects/wallet/AccountActionsBottomSheet.ts
+++ b/tests/page-objects/wallet/AccountActionsBottomSheet.ts
@@ -5,38 +5,39 @@ import Utilities from '../../framework/Utilities';
import EditAccountNameView from './EditAccountNameView';
import MultichainAccountDetails from '../MultichainAccounts/AccountDetails';
import MultichainEditAccountName from '../MultichainAccounts/EditAccountName';
+import { EncapsulatedElementType } from '../../framework';
class AccountActionsBottomSheet {
- get editAccount(): DetoxElement {
+ get editAccount(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.EDIT_ACCOUNT,
);
}
- get showPrivateKey(): DetoxElement {
+ get showPrivateKey(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.SHOW_PRIVATE_KEY,
);
}
- get switchToSmartAccount(): DetoxElement {
+ get switchToSmartAccount(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.SWITCH_TO_SMART_ACCOUNT,
);
}
- get showSrp(): DetoxElement {
+ get showSrp(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.SHOW_SECRET_RECOVERY_PHRASE,
);
}
- get multichainEditName(): DetoxElement {
+ get multichainEditName(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.MULTICHAIN_EDIT_NAME,
);
}
- get multichainAccountDetails(): DetoxElement {
+ get multichainAccountDetails(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountActionsBottomSheetSelectorsIDs.MULTICHAIN_ACCOUNT_DETAILS,
);
diff --git a/tests/page-objects/wallet/AccountListBottomSheet.ts b/tests/page-objects/wallet/AccountListBottomSheet.ts
index ab69121e8cf..142d73a2362 100644
--- a/tests/page-objects/wallet/AccountListBottomSheet.ts
+++ b/tests/page-objects/wallet/AccountListBottomSheet.ts
@@ -44,24 +44,24 @@ class AccountListBottomSheet {
});
}
- get accountTypeLabel(): DetoxElement {
+ get accountTypeLabel(): EncapsulatedElementType {
return Matchers.getElementByID(
AccountListBottomSheetSelectorsIDs.ACCOUNT_TYPE_LABEL,
);
}
- get accountTagLabel(): DetoxElement {
+ get accountTagLabel(): EncapsulatedElementType {
return Matchers.getElementByID(CellComponentSelectorsIDs.TAG_LABEL);
}
- get title(): DetoxElement {
+ get title(): EncapsulatedElementType {
return Matchers.getElementByText(
AccountListBottomSheetSelectorsText.ACCOUNTS_LIST_TITLE,
);
}
/** Header back control (same testID as CommonView.backButton / AccountSelector HeaderCompactStandard). */
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.BACK_ARROW_BUTTON);
}
@@ -80,19 +80,19 @@ class AccountListBottomSheet {
});
}
- get addEthereumAccountButton(): DetoxElement {
+ get addEthereumAccountButton(): EncapsulatedElementType {
return Matchers.getElementByText(
AccountListBottomSheetSelectorsText.ADD_ETHEREUM_ACCOUNT,
);
}
- get removeAccountAlertText(): DetoxElement {
+ get removeAccountAlertText(): EncapsulatedElementType {
return Matchers.getElementByText(
AccountListBottomSheetSelectorsText.REMOVE_IMPORTED_ACCOUNT,
);
}
- get connectAccountsButton(): DetoxElement {
+ get connectAccountsButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ConnectAccountBottomSheetSelectorsIDs.SELECT_MULTI_BUTTON,
);
@@ -122,15 +122,17 @@ class AccountListBottomSheet {
);
}
- getAccountElementByAccountNameV2(accountName: string): DetoxElement {
+ getAccountElementByAccountNameV2(
+ accountName: string,
+ ): EncapsulatedElementType {
return Matchers.getElementByIDAndLabel(AccountCellIds.ADDRESS, accountName);
}
- async getSelectElement(index: number): DetoxElement {
+ getSelectElement(index: number): EncapsulatedElementType {
return Matchers.getElementByID(CellComponentSelectorsIDs.SELECT, index);
}
- async getMultiselectElement(index: number): Promise {
+ getMultiselectElement(index: number): EncapsulatedElementType {
return Matchers.getElementByID(
CellComponentSelectorsIDs.MULTISELECT,
index,
@@ -145,7 +147,7 @@ class AccountListBottomSheet {
* @param {number} index - The index of the element to retrieve.
* @returns {Detox.IndexableNativeElement} The matcher for the element's title/name.
*/
- getSelectWithMenuElementName(index: number): DetoxElement {
+ getSelectWithMenuElementName(index: number): EncapsulatedElementType {
return Matchers.getElementByID(CellComponentSelectorsIDs.BASE_TITLE, index);
}
@@ -317,7 +319,7 @@ class AccountListBottomSheet {
}
// V2 Multichain Accounts Methods
- get ellipsisMenuButton(): DetoxElement {
+ get ellipsisMenuButton(): EncapsulatedElementType {
return Matchers.getElementByID(AccountCellIds.MENU);
}
diff --git a/tests/page-objects/wallet/AddAccountBottomSheet.ts b/tests/page-objects/wallet/AddAccountBottomSheet.ts
index a2d00b69768..b19e1cd8188 100644
--- a/tests/page-objects/wallet/AddAccountBottomSheet.ts
+++ b/tests/page-objects/wallet/AddAccountBottomSheet.ts
@@ -13,7 +13,7 @@ const AddAccountBottomSheetSelectorsIDs = {
};
class AddAccountBottomSheet {
- get importAccountButton(): DetoxElement {
+ get importAccountButton(): EncapsulatedElementType {
return Matchers.getElementByID(
AddAccountBottomSheetSelectorsIDs.IMPORT_ACCOUNT_BUTTON,
);
diff --git a/tests/page-objects/wallet/DefiPositionView.ts b/tests/page-objects/wallet/DefiPositionView.ts
index cb182bc788c..6676dae104e 100644
--- a/tests/page-objects/wallet/DefiPositionView.ts
+++ b/tests/page-objects/wallet/DefiPositionView.ts
@@ -2,6 +2,7 @@ import { WalletViewSelectorsIDs } from '../../../app/components/Views/Wallet/Wal
import { CommonSelectorsIDs } from '../../../app/util/Common.testIds';
import Assertions from '../../framework/Assertions';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
/**
* Page Object for the DeFi position details screen (e.g. "Aave V3").
@@ -9,14 +10,14 @@ import Matchers from '../../framework/Matchers';
*/
class DefiPositionView {
/** Main container of the position details screen */
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_DETAILS_CONTAINER,
);
}
/** Back arrow button in the navbar (navigates to DeFi list) */
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.BACK_ARROW_BUTTON);
}
diff --git a/tests/page-objects/wallet/DefiView.ts b/tests/page-objects/wallet/DefiView.ts
index 3e66ff64a79..048252a0df6 100644
--- a/tests/page-objects/wallet/DefiView.ts
+++ b/tests/page-objects/wallet/DefiView.ts
@@ -1,7 +1,7 @@
import { WalletViewSelectorsIDs } from '../../../app/components/Views/Wallet/WalletView.testIds';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
-import { Assertions } from '../../framework';
+import { Assertions, EncapsulatedElementType } from '../../framework';
/**
* Page Object for the DeFi full view screen.
@@ -10,14 +10,14 @@ import { Assertions } from '../../framework';
*/
class DefiView {
/** Main container wrapping the DeFi positions list and control bar */
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_CONTAINER,
);
}
/** Network filter button ("Popular networks" with chevron) */
- get networkFilter(): DetoxElement {
+ get networkFilter(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_NETWORK_FILTER,
);
diff --git a/tests/page-objects/wallet/EditAccountNameView.ts b/tests/page-objects/wallet/EditAccountNameView.ts
index 722090caf52..230189e0b3a 100644
--- a/tests/page-objects/wallet/EditAccountNameView.ts
+++ b/tests/page-objects/wallet/EditAccountNameView.ts
@@ -1,14 +1,15 @@
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { EditAccountNameSelectorIDs } from '../../../app/components/Views/EditAccountName/EditAccountName.testIds';
+import { EncapsulatedElementType } from '../../framework';
class EditAccountNameView {
- get saveButton(): DetoxElement {
+ get saveButton(): EncapsulatedElementType {
return Matchers.getElementByID(
EditAccountNameSelectorIDs.EDIT_ACCOUNT_NAME_SAVE,
);
}
- get accountNameInput(): DetoxElement {
+ get accountNameInput(): EncapsulatedElementType {
return Matchers.getElementByID(
EditAccountNameSelectorIDs.ACCOUNT_NAME_INPUT,
);
diff --git a/tests/page-objects/wallet/HomeSections.ts b/tests/page-objects/wallet/HomeSections.ts
index 3c086d58283..7285478bc2a 100644
--- a/tests/page-objects/wallet/HomeSections.ts
+++ b/tests/page-objects/wallet/HomeSections.ts
@@ -1,4 +1,9 @@
-import { Assertions, Gestures, Matchers } from '../../framework';
+import {
+ Assertions,
+ Gestures,
+ Matchers,
+ EncapsulatedElementType,
+} from '../../framework';
import {
WalletAssetSelectorsIDs,
WalletAssetSelectorsRegex,
@@ -13,18 +18,18 @@ class TokensFullView {
/**
* Back button in the tokens full view header
*/
- get backButton(): DetoxElement {
+ get backButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.BACK_BUTTON);
}
/**
* Network filter button in the tokens full view control bar
*/
- get networkFilterButton(): DetoxElement {
+ get networkFilterButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER);
}
- get stakedEthereumAssetRow(): DetoxElement {
+ get stakedEthereumAssetRow(): EncapsulatedElementType {
return Promise.resolve(
element(
by
diff --git a/tests/page-objects/wallet/ImportNFTFlow/ImportNFTView.ts b/tests/page-objects/wallet/ImportNFTFlow/ImportNFTView.ts
index 03ed9ccd957..b8b2a7abc47 100644
--- a/tests/page-objects/wallet/ImportNFTFlow/ImportNFTView.ts
+++ b/tests/page-objects/wallet/ImportNFTFlow/ImportNFTView.ts
@@ -1,25 +1,26 @@
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
import { NFTImportScreenSelectorsIDs } from '../../../../app/components/Views/AddAsset/ImportAssetView.testIds';
+import { EncapsulatedElementType } from '../../../framework';
class ImportNFTView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(NFTImportScreenSelectorsIDs.CONTAINER);
}
- get addressInput(): DetoxElement {
+ get addressInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NFTImportScreenSelectorsIDs.ADDRESS_INPUT_BOX,
);
}
- get addressWarningMessage(): DetoxElement {
+ get addressWarningMessage(): EncapsulatedElementType {
return Matchers.getElementByID(
NFTImportScreenSelectorsIDs.ADDRESS_WARNING_MESSAGE,
);
}
- get identifierInput(): DetoxElement {
+ get identifierInput(): EncapsulatedElementType {
return Matchers.getElementByID(
NFTImportScreenSelectorsIDs.IDENTIFIER_INPUT_BOX,
);
diff --git a/tests/page-objects/wallet/ImportTokenFlow/ConfirmAddAsset.ts b/tests/page-objects/wallet/ImportTokenFlow/ConfirmAddAsset.ts
index 72d77db765b..fd419aa8aa5 100644
--- a/tests/page-objects/wallet/ImportTokenFlow/ConfirmAddAsset.ts
+++ b/tests/page-objects/wallet/ImportTokenFlow/ConfirmAddAsset.ts
@@ -4,33 +4,34 @@ import {
} from '../../../../app/components/Views/AddAsset/ImportAssetView.testIds';
import Matchers from '../../../framework/Matchers';
import Gestures from '../../../framework/Gestures';
+import { EncapsulatedElementType } from '../../../framework';
class ConfirmAddAssetView {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportTokenViewSelectorsIDs.ADD_CONFIRM_CUSTOM_ASSET,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByText(
ImportTokenViewSelectorsText.CANCEL_IMPORT_TOKEN,
);
}
- get confirmButton(): DetoxElement {
+ get confirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportTokenViewSelectorsIDs.BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT,
);
}
- get cancelModal(): DetoxElement {
+ get cancelModal(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportTokenViewSelectorsIDs.ADD_CANCEL_ADD_CUSTOM_ASSET_MODAL,
);
}
- get confirmButtonModal(): DetoxElement {
+ get confirmButtonModal(): EncapsulatedElementType {
return Matchers.getElementByText(
ImportTokenViewSelectorsText.CONFIRM_CANCEL_IMPORT_TOKEN,
);
diff --git a/tests/page-objects/wallet/ImportTokenFlow/ImportTokensView.ts b/tests/page-objects/wallet/ImportTokenFlow/ImportTokensView.ts
index 09c88402653..09a0c7cfecb 100644
--- a/tests/page-objects/wallet/ImportTokenFlow/ImportTokensView.ts
+++ b/tests/page-objects/wallet/ImportTokenFlow/ImportTokensView.ts
@@ -5,52 +5,52 @@ import {
ImportTokenViewSelectorsText,
} from '../../../../app/components/Views/AddAsset/ImportAssetView.testIds';
import { CellComponentSelectorsIDs } from '../../../../app/component-library/components/Cells/Cell/CellComponent.testIds';
-import { logger } from '../../../framework';
+import { logger, EncapsulatedElementType } from '../../../framework';
class ImportTokensView {
- get searchTokenResult(): DetoxElement {
+ get searchTokenResult(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportTokenViewSelectorsIDs.SEARCH_TOKEN_RESULT,
);
}
- get nextButton(): DetoxElement {
+ get nextButton(): EncapsulatedElementType {
return Matchers.getElementByID(ImportTokenViewSelectorsIDs.NEXT_BUTTON);
}
- get networkInput(): DetoxElement {
+ get networkInput(): EncapsulatedElementType {
return Matchers.getElementByID(
ImportTokenViewSelectorsIDs.SELECT_NETWORK_BUTTON,
);
}
- get symbolInput(): DetoxElement {
+ get symbolInput(): EncapsulatedElementType {
return Matchers.getElementByID(ImportTokenViewSelectorsIDs.SYMBOL_INPUT);
}
- get tokenSymbolText(): DetoxElement {
+ get tokenSymbolText(): EncapsulatedElementType {
return Matchers.getElementByText(ImportTokenViewSelectorsText.TOKEN_SYMBOL);
}
- get addressInput(): DetoxElement {
+ get addressInput(): EncapsulatedElementType {
return Matchers.getElementByID(ImportTokenViewSelectorsIDs.ADDRESS_INPUT);
}
- get decimalInput(): DetoxElement {
+ get decimalInput(): EncapsulatedElementType {
return Matchers.getElementByID(ImportTokenViewSelectorsIDs.DECIMAL_INPUT);
}
- get customTokenTab(): DetoxElement {
+ get customTokenTab(): EncapsulatedElementType {
return Matchers.getElementByText(
ImportTokenViewSelectorsText.CUSTOM_TOKEN_TAB,
);
}
- get searchTokenBar(): DetoxElement {
+ get searchTokenBar(): EncapsulatedElementType {
return Matchers.getElementByID(ImportTokenViewSelectorsIDs.SEARCH_BAR);
}
- get networkList(): DetoxElement {
+ get networkList(): EncapsulatedElementType {
return Matchers.getElementByID(CellComponentSelectorsIDs.SELECT, 0);
}
diff --git a/tests/page-objects/wallet/LoginView.ts b/tests/page-objects/wallet/LoginView.ts
index 83046d0bbef..7086ca7b5ea 100644
--- a/tests/page-objects/wallet/LoginView.ts
+++ b/tests/page-objects/wallet/LoginView.ts
@@ -44,11 +44,11 @@ class LoginView {
});
}
- get forgotPasswordButton(): DetoxElement {
+ get forgotPasswordButton(): EncapsulatedElementType {
return Matchers.getElementByID(LoginViewSelectors.RESET_WALLET);
}
- get rememberMeSwitch(): DetoxElement {
+ get rememberMeSwitch(): EncapsulatedElementType {
return Matchers.getElementByID(LoginViewSelectors.REMEMBER_ME_SWITCH);
}
diff --git a/tests/page-objects/wallet/MultiSrp/Common/SRPListComponent.ts b/tests/page-objects/wallet/MultiSrp/Common/SRPListComponent.ts
index 57351faac4b..fd7e5cf0a95 100644
--- a/tests/page-objects/wallet/MultiSrp/Common/SRPListComponent.ts
+++ b/tests/page-objects/wallet/MultiSrp/Common/SRPListComponent.ts
@@ -1,8 +1,9 @@
import { SRPListSelectorsIDs } from '../../../../../app/components/UI/SRPList/SRPList.testIds';
import Matchers from '../../../../framework/Matchers';
+import { EncapsulatedElementType } from '../../../../framework';
class SRPListComponent {
- get srpList(): DetoxElement {
+ get srpList(): EncapsulatedElementType {
return Matchers.getElementByID(SRPListSelectorsIDs.SRP_LIST);
}
}
diff --git a/tests/page-objects/wallet/NetworkManager.ts b/tests/page-objects/wallet/NetworkManager.ts
index f62e9c5bb2f..e3bd0c9cad3 100644
--- a/tests/page-objects/wallet/NetworkManager.ts
+++ b/tests/page-objects/wallet/NetworkManager.ts
@@ -12,19 +12,20 @@ import {
WalletViewSelectorsIDs,
WalletViewSelectorsText,
} from '../../../app/components/Views/Wallet/WalletView.testIds';
+import { EncapsulatedElementType } from '../../framework';
class NetworkManager {
/**
* Button to open the network manager
*/
- get openNetworkManagerButton(): DetoxElement {
+ get openNetworkManagerButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER);
}
/**
* Select the bottom sheet of the network manager
*/
- get networkManagerBottomSheet(): DetoxElement {
+ get networkManagerBottomSheet(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.NETWORK_MANAGER_BOTTOM_SHEET,
);
@@ -33,7 +34,7 @@ class NetworkManager {
/**
* Select the tab of the popular networks
*/
- get popularNetworksTab(): DetoxElement {
+ get popularNetworksTab(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkManagerSelectorText.POPULAR_NETWORKS_TAB,
);
@@ -42,7 +43,7 @@ class NetworkManager {
/**
* Select the tab of the custom networks
*/
- get customNetworksTab(): DetoxElement {
+ get customNetworksTab(): EncapsulatedElementType {
return Matchers.getElementByText(
NetworkManagerSelectorText.CUSTOM_NETWORKS_TAB,
);
@@ -51,7 +52,7 @@ class NetworkManager {
/**
* Select the container of the popular networks tab
*/
- get popularNetworksContainer(): DetoxElement {
+ get popularNetworksContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.POPULAR_NETWORKS_CONTAINER,
);
@@ -60,7 +61,7 @@ class NetworkManager {
/**
* Select the container of the custom networks tab
*/
- get customNetworksContainer(): DetoxElement {
+ get customNetworksContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.CUSTOM_NETWORKS_CONTAINER,
);
@@ -69,7 +70,7 @@ class NetworkManager {
/**
* Select the button to select all popular networks
*/
- get selectAllPopularNetworksSelected(): DetoxElement {
+ get selectAllPopularNetworksSelected(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.SELECT_ALL_POPULAR_NETWORKS_SELECTED,
);
@@ -78,7 +79,7 @@ class NetworkManager {
/**
* Select the button to select all popular networks
*/
- get selectAllPopularNetworksNotSelected(): DetoxElement {
+ get selectAllPopularNetworksNotSelected(): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.SELECT_ALL_POPULAR_NETWORKS_NOT_SELECTED,
);
@@ -87,7 +88,7 @@ class NetworkManager {
/**
* Select the network by name wether it is selected or not
*/
- getNetworkByCaipChainId(caipChainId: CaipChainId): DetoxElement {
+ getNetworkByCaipChainId(caipChainId: CaipChainId): EncapsulatedElementType {
return Matchers.getElementByID(
new RegExp(`^network-list-item-${caipChainId}-(selected|not-selected)$`),
);
@@ -96,7 +97,9 @@ class NetworkManager {
/**
* Select the network by name if it is selected
*/
- getSelectedNetworkByCaipChainId(caipChainId: CaipChainId): DetoxElement {
+ getSelectedNetworkByCaipChainId(
+ caipChainId: CaipChainId,
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.NETWORK_LIST_ITEM(caipChainId, true),
);
@@ -105,7 +108,9 @@ class NetworkManager {
/**
* Select the network by name if it is not selected
*/
- getNotSelectedNetworkByCaipChainId(caipChainId: CaipChainId): DetoxElement {
+ getNotSelectedNetworkByCaipChainId(
+ caipChainId: CaipChainId,
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
NetworkManagerSelectorIDs.NETWORK_LIST_ITEM(caipChainId, false),
);
@@ -114,7 +119,7 @@ class NetworkManager {
/**
* Select the network by name in the base control bar
*/
- getBaseControlBarText(caipChainId: CaipChainId): DetoxElement {
+ getBaseControlBarText(caipChainId: CaipChainId): EncapsulatedElementType {
return Matchers.getElementByID(
`${WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER}-${caipChainId}`,
);
@@ -124,7 +129,7 @@ class NetworkManager {
* Get token element by symbol
* Note: Gets the first instance in case of duplicates during render cycles
*/
- getTokenBySymbol(symbol: string): DetoxElement {
+ getTokenBySymbol(symbol: string): EncapsulatedElementType {
return Matchers.getElementByID(`asset-${symbol}`, 0);
}
@@ -343,7 +348,7 @@ class NetworkManager {
/**
* Get network by display name (for custom networks)
*/
- getNetworkByName(networkName: string): DetoxElement {
+ getNetworkByName(networkName: string): EncapsulatedElementType {
return Matchers.getElementByText(networkName);
}
diff --git a/tests/page-objects/wallet/NftDetectionModal.ts b/tests/page-objects/wallet/NftDetectionModal.ts
index a3532908960..65accfee3fa 100644
--- a/tests/page-objects/wallet/NftDetectionModal.ts
+++ b/tests/page-objects/wallet/NftDetectionModal.ts
@@ -1,17 +1,18 @@
import { NftDetectionModalSelectorsIDs } from '../../../app/components/Views/NFTAutoDetectionModal/NftDetectionModal.testIds';
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
+import { EncapsulatedElementType } from '../../framework';
class NftDetectionModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(NftDetectionModalSelectorsIDs.CONTAINER);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(NftDetectionModalSelectorsIDs.CANCEL_BUTTON);
}
- get allowButton(): DetoxElement {
+ get allowButton(): EncapsulatedElementType {
return Matchers.getElementByID(NftDetectionModalSelectorsIDs.ALLOW_BUTTON);
}
diff --git a/tests/page-objects/wallet/SelectNetworkBottomSheet.ts b/tests/page-objects/wallet/SelectNetworkBottomSheet.ts
index 0205a813131..33e1b8210a0 100644
--- a/tests/page-objects/wallet/SelectNetworkBottomSheet.ts
+++ b/tests/page-objects/wallet/SelectNetworkBottomSheet.ts
@@ -1,9 +1,10 @@
import { PermissionSummaryBottomSheetSelectorsText } from '../../../app/components/Views/MultichainAccounts/shared/PermissionSummaryBottomSheet.testIds';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class SelectNetworksBottomSheet {
- get connectedAccountsText(): DetoxElement {
+ get connectedAccountsText(): EncapsulatedElementType {
return Matchers.getElementByText(
PermissionSummaryBottomSheetSelectorsText.CONNECTED_ACCOUNTS_TEXT,
);
diff --git a/tests/page-objects/wallet/SendActionBottomSheet.ts b/tests/page-objects/wallet/SendActionBottomSheet.ts
index 75176f11209..75d1d88b9b8 100644
--- a/tests/page-objects/wallet/SendActionBottomSheet.ts
+++ b/tests/page-objects/wallet/SendActionBottomSheet.ts
@@ -1,39 +1,40 @@
import { SendActionViewSelectorsIDs } from '../../selectors/SendFlow/SendActionView.selectors';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class SendActionBottomSheet {
- get solanaAddressInputField(): DetoxElement {
+ get solanaAddressInputField(): EncapsulatedElementType {
return Matchers.getElementByID(
SendActionViewSelectorsIDs.SOLANA_INPUT_ADDRESS_FIELD,
);
}
- get solanaAmountInputField(): DetoxElement {
+ get solanaAmountInputField(): EncapsulatedElementType {
return Matchers.getElementByID(
SendActionViewSelectorsIDs.SOLANA_INPUT_AMOUNT_FIELD,
);
}
- get invalidAddressError(): DetoxElement {
+ get invalidAddressError(): EncapsulatedElementType {
return Matchers.getElementByID(
SendActionViewSelectorsIDs.INVALID_ADDRESS_ERROR,
);
}
- get continueButton(): DetoxElement {
+ get continueButton(): EncapsulatedElementType {
return Matchers.getElementByID(SendActionViewSelectorsIDs.CONTINUE_BUTTON);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByID(SendActionViewSelectorsIDs.CANCEL_BUTTON);
}
- get closeModalButton(): DetoxElement {
+ get closeModalButton(): EncapsulatedElementType {
return Matchers.getElementByID(SendActionViewSelectorsIDs.CLOSE_BUTTON);
}
- get sendSOLTransactionButton(): DetoxElement {
+ get sendSOLTransactionButton(): EncapsulatedElementType {
return Matchers.getElementByID(
SendActionViewSelectorsIDs.SEND_TRANSACTION_BUTTON,
);
diff --git a/tests/page-objects/wallet/ToastModal.ts b/tests/page-objects/wallet/ToastModal.ts
index 494121d026b..186a24087ae 100644
--- a/tests/page-objects/wallet/ToastModal.ts
+++ b/tests/page-objects/wallet/ToastModal.ts
@@ -21,7 +21,7 @@ const DEFAULT_TOAST_APPEAR_TIMEOUT_MS = 5_000;
const TOAST_POLL_INTERVAL_MS = 250;
class ToastModal {
- get container(): DetoxElement {
+ get container(): EncapsulatedElementType {
return Matchers.getElementByID(ToastSelectorsIDs.CONTAINER);
}
@@ -35,11 +35,11 @@ class ToastModal {
});
}
- get notificationTitle(): DetoxElement {
+ get notificationTitle(): EncapsulatedElementType {
return Matchers.getElementByID(ToastSelectorsIDs.NOTIFICATION_TITLE);
}
- get toastCloseButton(): DetoxElement {
+ get toastCloseButton(): EncapsulatedElementType {
return Matchers.getElementByText(ToastSelectorsText.CLOSE_BUTTON);
}
diff --git a/tests/page-objects/wallet/TokenOverview.ts b/tests/page-objects/wallet/TokenOverview.ts
index a42ae755d72..7bf2367b0c6 100644
--- a/tests/page-objects/wallet/TokenOverview.ts
+++ b/tests/page-objects/wallet/TokenOverview.ts
@@ -33,7 +33,7 @@ class TokenOverview {
});
}
- get tokenPrice(): DetoxElement {
+ get tokenPrice(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.TOKEN_PRICE);
}
@@ -91,73 +91,73 @@ class TokenOverview {
});
}
- get unstakeButton(): DetoxElement {
+ get unstakeButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.UNSTAKE_BUTTON);
}
- get stakeMoreButton(): DetoxElement {
+ get stakeMoreButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.STAKE_MORE_BUTTON);
}
- get stakedBalance(): DetoxElement {
+ get stakedBalance(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsText.STAKED_BALANCE);
}
- get actionSheetSendButton(): DetoxElement {
+ get actionSheetSendButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON,
);
}
- get swapButton(): DetoxElement {
+ get swapButton(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.SWAP_BUTTON);
}
- get bridgeButton(): DetoxElement {
+ get bridgeButton(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.BRIDGE_BUTTON);
}
- get claimButton(): DetoxElement {
+ get claimButton(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.CLAIM_BUTTON);
}
- get receiveButton(): DetoxElement {
+ get receiveButton(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.RECEIVE_BUTTON);
}
- get noChartData(): DetoxElement {
+ get noChartData(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText.NO_CHART_DATA);
}
- get closeButton(): DetoxElement {
+ get closeButton(): EncapsulatedElementType {
return Matchers.getElementByID(CommonSelectorsIDs.BACK_ARROW_BUTTON);
}
- get unstakingBanner(): DetoxElement {
+ get unstakingBanner(): EncapsulatedElementType {
return Matchers.getElementByID(TokenOverviewSelectorsIDs.UNSTAKING_BANNER);
}
- get chartPeriod1d(): DetoxElement {
+ get chartPeriod1d(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['1d']);
}
- get chartPeriod1w(): DetoxElement {
+ get chartPeriod1w(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['1w']);
}
- get chartPeriod1m(): DetoxElement {
+ get chartPeriod1m(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['1m']);
}
- get chartPeriod3m(): DetoxElement {
+ get chartPeriod3m(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['3m']);
}
- get chartPeriod1y(): DetoxElement {
+ get chartPeriod1y(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['1y']);
}
- get chartPeriod3y(): DetoxElement {
+ get chartPeriod3y(): EncapsulatedElementType {
return Matchers.getElementByText(TokenOverviewSelectorsText['3y']);
}
diff --git a/tests/page-objects/wallet/TokenSortBottomSheet.ts b/tests/page-objects/wallet/TokenSortBottomSheet.ts
index d4a4e70d30f..97fd4eb7326 100644
--- a/tests/page-objects/wallet/TokenSortBottomSheet.ts
+++ b/tests/page-objects/wallet/TokenSortBottomSheet.ts
@@ -1,13 +1,14 @@
import { WalletViewSelectorsIDs } from '../../../app/components/Views/Wallet/WalletView.testIds';
import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
+import { EncapsulatedElementType } from '../../framework';
class SortModal {
- get sortAlphabetically(): DetoxElement {
+ get sortAlphabetically(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.SORT_ALPHABETICAL);
}
- get sortFiatAmount(): DetoxElement {
+ get sortFiatAmount(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.SORT_DECLINING_BALANCE,
);
diff --git a/tests/page-objects/wallet/TokensView.ts b/tests/page-objects/wallet/TokensView.ts
index 6a39d1e92ba..be3197dad78 100644
--- a/tests/page-objects/wallet/TokensView.ts
+++ b/tests/page-objects/wallet/TokensView.ts
@@ -5,13 +5,14 @@ import Gestures from '../../framework/Gestures';
import Matchers from '../../framework/Matchers';
import Utilities from '../../framework/Utilities';
import NetworkManager from './NetworkManager';
+import { EncapsulatedElementType } from '../../framework';
class TokensView {
- get networkFilter(): DetoxElement {
+ get networkFilter(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER);
}
- earnCtaForToken(tokenSymbol: string): DetoxElement {
+ earnCtaForToken(tokenSymbol: string): EncapsulatedElementType {
return Matchers.getElementIDWithAncestor(
SECONDARY_BALANCE_BUTTON_TEST_ID,
getAssetTestId(tokenSymbol),
diff --git a/tests/page-objects/wallet/WalletActionsBottomSheet.ts b/tests/page-objects/wallet/WalletActionsBottomSheet.ts
index 333a3ed67e8..d7bad8415b3 100644
--- a/tests/page-objects/wallet/WalletActionsBottomSheet.ts
+++ b/tests/page-objects/wallet/WalletActionsBottomSheet.ts
@@ -11,37 +11,37 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
import { encapsulatedAction } from '../../framework/encapsulatedAction';
class WalletActionsBottomSheet {
- get sendButton(): DetoxElement {
+ get sendButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON,
);
}
- get receiveButton(): DetoxElement {
+ get receiveButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.RECEIVE_BUTTON,
);
}
- get swapButton(): DetoxElement {
+ get swapButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON,
);
}
- get bridgeButton(): DetoxElement {
+ get bridgeButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.BRIDGE_BUTTON,
);
}
- get buyButton(): DetoxElement {
+ get buyButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON,
);
}
- get sellButton(): DetoxElement {
+ get sellButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletActionsBottomSheetSelectorsIDs.SELL_BUTTON,
);
diff --git a/tests/page-objects/wallet/WalletView.ts b/tests/page-objects/wallet/WalletView.ts
index f83fd027cd0..cbec5d41f25 100644
--- a/tests/page-objects/wallet/WalletView.ts
+++ b/tests/page-objects/wallet/WalletView.ts
@@ -58,7 +58,7 @@ class WalletView {
}
/** Wallet ScrollView as element (for gestures like swipe). */
- get walletScrollView(): DetoxElement {
+ get walletScrollView(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.WALLET_SCROLL_VIEW);
}
@@ -70,7 +70,7 @@ class WalletView {
* from the tab bar (e.g. direction 'up' = one more scroll down = section moves higher on screen).
*/
private async scrollAndTapSection(
- target: DetoxElement,
+ target: DetoxElement | EncapsulatedElementType,
description: string,
direction: 'up' | 'down' = 'down',
options: {
@@ -96,7 +96,7 @@ class WalletView {
});
}
- get earnButton(): DetoxElement {
+ get earnButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.STAKE_BUTTON);
}
@@ -104,19 +104,19 @@ class WalletView {
* The "Earn" CTA on the USDC token row's secondary balance area.
* Index 2 = USDC (third token: ETH → mUSD → USDC) in the standard lending fixture.
*/
- get lendingEarnCta(): DetoxElement {
+ get lendingEarnCta(): EncapsulatedElementType {
return Matchers.getElementByID(SECONDARY_BALANCE_BUTTON_TEST_ID, 2);
}
- get stakedEthereumLabel(): DetoxElement {
+ get stakedEthereumLabel(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.STAKED_ETHEREUM);
}
- get stakeMoreButton(): DetoxElement {
+ get stakeMoreButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.STAKE_MORE_BUTTON);
}
- get tokenDetectionLinkButton(): DetoxElement {
+ get tokenDetectionLinkButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.WALLET_TOKEN_DETECTION_LINK_BUTTON,
);
@@ -138,11 +138,11 @@ class WalletView {
});
}
- get eyeSlashIcon(): DetoxElement {
+ get eyeSlashIcon(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.EYE_SLASH_ICON);
}
- get notificationBellIcon(): DetoxElement {
+ get notificationBellIcon(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.WALLET_NOTIFICATIONS_BUTTON,
);
@@ -162,7 +162,7 @@ class WalletView {
});
}
- get navbarNetworkText(): DetoxElement {
+ get navbarNetworkText(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT);
}
@@ -178,33 +178,33 @@ class WalletView {
});
}
- get navbarNetworkPicker(): DetoxElement {
+ get navbarNetworkPicker(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER,
);
}
- get navbarCardButton(): DetoxElement {
+ get navbarCardButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.CARD_BUTTON);
}
- get nftTab(): DetoxElement {
+ get nftTab(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.NFTS_TAB);
}
- get nftTabContainer(): DetoxElement {
+ get nftTabContainer(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.NFT_TAB_CONTAINER);
}
- get importNFTButton(): DetoxElement {
+ get importNFTButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.IMPORT_NFT_BUTTON);
}
- get importTokensButton(): DetoxElement {
+ get importTokensButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.IMPORT_TOKEN_BUTTON);
}
- get networkName(): DetoxElement {
+ get networkName(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.NETWORK_NAME);
}
@@ -231,7 +231,7 @@ class WalletView {
});
}
- get accountName(): DetoxElement {
+ get accountName(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_TEXT,
);
@@ -262,57 +262,57 @@ class WalletView {
});
}
- get hideTokensLabel(): DetoxElement {
+ get hideTokensLabel(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.HIDE_TOKENS);
}
- get currentMainWalletAccountActions(): DetoxElement {
+ get currentMainWalletAccountActions(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_TEXT,
);
}
- get tokenNetworkFilter(): DetoxElement {
+ get tokenNetworkFilter(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER);
}
- get sortButton(): DetoxElement {
+ get sortButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.SORT_BUTTON);
}
- get sortBy(): DetoxElement {
+ get sortBy(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.SORT_BY);
}
- get tokenNetworkFilterAll(): DetoxElement {
+ get tokenNetworkFilterAll(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER_ALL,
);
}
- get tokenNetworkFilterCurrent(): DetoxElement {
+ get tokenNetworkFilterCurrent(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.TOKEN_NETWORK_FILTER_CURRENT,
);
}
- get cancelButton(): DetoxElement {
+ get cancelButton(): EncapsulatedElementType {
return Matchers.getElementByText('Cancel');
}
- get carouselContainer(): DetoxElement {
+ get carouselContainer(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.CAROUSEL_CONTAINER);
}
- get carouselProgressDots(): DetoxElement {
+ get carouselProgressDots(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.CAROUSEL_PROGRESS_DOTS,
);
}
- get testCollectible(): DetoxElement {
+ get testCollectible(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.TEST_COLLECTIBLE, 1);
}
- get testCollectibleFallback(): DetoxElement {
+ get testCollectibleFallback(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.COLLECTIBLE_FALLBACK,
1,
@@ -349,7 +349,7 @@ class WalletView {
});
}
- get walletBridgeButton(): DetoxElement {
+ get walletBridgeButton(): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.WALLET_BRIDGE_BUTTON);
}
@@ -372,77 +372,81 @@ class WalletView {
}
// mUSD conversion (Earn) - asset list CTA, education screen, token list CTA, asset overview CTA
- get musdConversionCta(): DetoxElement {
+ get musdConversionCta(): EncapsulatedElementType {
return Matchers.getElementByID(
EARN_TEST_IDS.MUSD.ASSET_LIST_CONVERSION_CTA,
);
}
- get cashGetMusdContainer(): DetoxElement {
+ get cashGetMusdContainer(): EncapsulatedElementType {
return Matchers.getElementByID(CashGetMusdEmptyStateSelectors.CONTAINER);
}
- get getMusdButton(): DetoxElement {
+ get getMusdButton(): EncapsulatedElementType {
return Matchers.getElementByText('Get mUSD');
}
- get getStartedButton(): DetoxElement {
+ get getStartedButton(): EncapsulatedElementType {
return Matchers.getElementByText('Get Started');
}
/** Token list item CTA: "Get 3% mUSD bonus" on USDC row. Use testID + index (1 = USDC after ETH) to avoid regex/text flakiness. */
- get tokenListItemConvertToMusdCta(): DetoxElement {
+ get tokenListItemConvertToMusdCta(): EncapsulatedElementType {
return Matchers.getElementByID(SECONDARY_BALANCE_BUTTON_TEST_ID, 1);
}
- get assetOverviewMusdCta(): DetoxElement {
+ get assetOverviewMusdCta(): EncapsulatedElementType {
return Matchers.getElementByID(
EARN_TEST_IDS.MUSD.ASSET_OVERVIEW_CONVERSION_CTA,
);
}
- get walletReceiveButton(): DetoxElement {
+ get walletReceiveButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.WALLET_RECEIVE_BUTTON,
);
}
// Balance Empty State - displayed when account group has zero balance across all networks
- get balanceEmptyStateContainer(): DetoxElement {
+ get balanceEmptyStateContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.BALANCE_EMPTY_STATE_CONTAINER,
);
}
- get balanceEmptyStateActionButton(): DetoxElement {
+ get balanceEmptyStateActionButton(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.BALANCE_EMPTY_STATE_ACTION_BUTTON,
);
}
- getPredictCurrentPositionCardByIndex(index: number = 0): DetoxElement {
+ getPredictCurrentPositionCardByIndex(
+ index: number = 0,
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
PredictPositionSelectorsIDs.CURRENT_POSITION_CARD,
index,
);
}
- getPredictResolvedPositionCardByIndex(index: number = 0): DetoxElement {
+ getPredictResolvedPositionCardByIndex(
+ index: number = 0,
+ ): EncapsulatedElementType {
return Matchers.getElementByID(
PredictPositionSelectorsIDs.RESOLVED_POSITION_CARD,
index,
);
}
- getCarouselSlide(id: string): DetoxElement {
+ getCarouselSlide(id: string): EncapsulatedElementType {
return Matchers.getElementByID(WalletViewSelectorsIDs.CAROUSEL_SLIDE(id));
}
- getCarouselSlideTitle(id: string): DetoxElement {
+ getCarouselSlideTitle(id: string): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.CAROUSEL_SLIDE_TITLE(id),
);
}
- getCarouselSlideCloseButton(id: string): DetoxElement {
+ getCarouselSlideCloseButton(id: string): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.CAROUSEL_SLIDE_CLOSE_BUTTON(id),
);
@@ -648,21 +652,21 @@ class WalletView {
});
}
- async tokenInWallet(tokenName: string): Promise {
+ async tokenInWallet(tokenName: string): Promise {
return Matchers.getElementByText(tokenName);
}
- async getTokensInWallet(): Promise {
+ async getTokensInWallet(): Promise {
return Matchers.getElementByID(
WalletViewSelectorsIDs.TOKENS_CONTAINER_LIST,
);
}
- async nftIDInWallet(nftId: string): Promise {
+ async nftIDInWallet(nftId: string): Promise {
return Matchers.getElementByID(nftId);
}
- async nftInWallet(nftName: string): Promise {
+ async nftInWallet(nftName: string): Promise {
return Matchers.getElementByText(nftName);
}
@@ -756,65 +760,65 @@ class WalletView {
});
}
- get defiTab(): DetoxElement {
+ get defiTab(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.DEFI_TAB);
}
- get defiNetworkFilter(): DetoxElement {
+ get defiNetworkFilter(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_NETWORK_FILTER,
);
}
- get defiTabContainer(): DetoxElement {
+ get defiTabContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_CONTAINER,
);
}
- get claimButton(): DetoxElement {
+ get claimButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictPositionsHeaderSelectorsIDs.CLAIM_BUTTON,
);
}
- get predictClaimConfirmButton(): DetoxElement {
+ get predictClaimConfirmButton(): EncapsulatedElementType {
return Matchers.getElementByID(
PredictClaimConfirmationSelectorsIDs.CLAIM_CONFIRM_BUTTON,
);
}
- get defiPositionDetailsContainer(): DetoxElement {
+ get defiPositionDetailsContainer(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.DEFI_POSITIONS_DETAILS_CONTAINER,
);
}
- get predictionsTab(): DetoxElement {
+ get predictionsTab(): EncapsulatedElementType {
return Matchers.getElementByLabel(WalletViewSelectorsText.PREDICTIONS_TAB);
}
- get availableBalanceLabel(): DetoxElement {
+ get availableBalanceLabel(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.AVAILABLE_BALANCE);
}
- get defiPositionsNew(): DetoxElement {
+ get defiPositionsNew(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.DEFI_SECTION);
}
/** Perpetuals section title button on the homepage. */
- get perpsSectionHeader(): DetoxElement {
+ get perpsSectionHeader(): EncapsulatedElementType {
return Matchers.getElementByLabel(
WalletViewSelectorsText.PERPETUALS_SECTION,
);
}
/** Predictions section title button on the homepage. */
- get predictionsSectionHeader(): DetoxElement {
+ get predictionsSectionHeader(): EncapsulatedElementType {
return Matchers.getElementByID(
WalletViewSelectorsIDs.HOMEPAGE_SECTION_TITLE('predictions'),
);
}
/** Tokens section header on the homepage. */
- get tokensSectionHeader(): DetoxElement {
+ get tokensSectionHeader(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.TOKENS_SECTION);
}
@@ -838,7 +842,7 @@ class WalletView {
}
/** NFTs section header on the homepage. */
- get nftsSectionHeader(): DetoxElement {
+ get nftsSectionHeader(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.NFTS_SECTION);
}
@@ -1251,7 +1255,7 @@ class WalletView {
});
}
- get perpsTab(): DetoxElement {
+ get perpsTab(): EncapsulatedElementType {
return Matchers.getElementByText(WalletViewSelectorsText.PERPS_TAB);
}
diff --git a/tests/regression/wallet/carousel.spec.ts b/tests/regression/wallet/carousel.spec.ts
index ab0ebc5ccf7..31ad73fe093 100644
--- a/tests/regression/wallet/carousel.spec.ts
+++ b/tests/regression/wallet/carousel.spec.ts
@@ -89,7 +89,8 @@ describe(RegressionWalletUX('Carousel Tests'), () => {
await device.disableSynchronization();
// Find and dismiss any available slide
- const firstSlide = await WalletView.carouselContainer;
+ const firstSlide =
+ (await WalletView.carouselContainer) as Detox.IndexableNativeElement;
await firstSlide.tap();
await Assertions.expectElementToBeVisible(
diff --git a/tsconfig.json b/tsconfig.json
index 3b00b1107c4..ce1292945ea 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -32,6 +32,22 @@
"@metamask/json-rpc-engine/v2": [
"node_modules/@metamask/json-rpc-engine/dist/v2/index.d.cts"
],
+ // TODO: Remove these once we use `Node16` module resolution.
+ "@metamask/keyring-api/v2": [
+ "node_modules/@metamask/keyring-api/dist/v2/index.d.cts"
+ ],
+ "@metamask/keyring-sdk/v2": [
+ "node_modules/@metamask/keyring-sdk/dist/v2/index.d.cts"
+ ],
+ "@metamask/eth-snap-keyring/v2": [
+ "node_modules/@metamask/eth-snap-keyring/dist/v2/index.d.cts"
+ ],
+ "@metamask/eth-hd-keyring/v2": [
+ "node_modules/@metamask/eth-hd-keyring/dist/v2/index.d.cts"
+ ],
+ "@metamask/eth-ledger-bridge-keyring/v2": [
+ "node_modules/@metamask/eth-ledger-bridge-keyring/dist/v2/index.d.cts"
+ ],
"@metamask/design-system-react-native/spinner": [
"node_modules/@metamask/design-system-react-native/dist/components/temp-components/Spinner/index.cjs"
],
diff --git a/yarn.lock b/yarn.lock
index c70660364a3..40a4abbc4a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7762,16 +7762,16 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/account-tree-controller@npm:^7.2.0, @metamask/account-tree-controller@npm:^7.5.1":
- version: 7.5.1
- resolution: "@metamask/account-tree-controller@npm:7.5.1"
+"@metamask/account-tree-controller@npm:^7.2.0, @metamask/account-tree-controller@npm:^7.5.0, @metamask/account-tree-controller@npm:^7.5.2":
+ version: 7.5.2
+ resolution: "@metamask/account-tree-controller@npm:7.5.2"
dependencies:
- "@metamask/accounts-controller": "npm:^39.0.0"
+ "@metamask/accounts-controller": "npm:^39.0.1"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/messenger": "npm:^1.2.0"
- "@metamask/multichain-account-service": "npm:^10.0.2"
+ "@metamask/multichain-account-service": "npm:^10.0.3"
"@metamask/profile-sync-controller": "npm:^28.1.1"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/snaps-sdk": "npm:^11.0.0"
@@ -7783,23 +7783,24 @@ __metadata:
peerDependencies:
"@metamask/providers": ^22.0.0
webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/e037c51b800f4c63fbb6ca17d2e2c5f606e5b2456b3435d8e58358e83791c598f2a80d789799371029b7db539baf29b6fe8ef6cb6db12bcf2bba2bf7f71670cf
+ checksum: 10/cb503b2b6eacbe1e7edfe88897d995c2d6190a042a13ec6248f312be3ebcb178d9d19ec9f74fe8bdc052e1627dfd675023c009e8e22b516f9f493c17cbad0c12
languageName: node
linkType: hard
-"@metamask/accounts-controller@npm:^38.0.0":
- version: 38.0.0
- resolution: "@metamask/accounts-controller@npm:38.0.0"
+"@metamask/accounts-controller@npm:^39.0.0":
+ version: 39.0.0
+ resolution: "@metamask/accounts-controller@npm:39.0.0"
dependencies:
"@ethereumjs/util": "npm:^9.1.0"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/eth-snap-keyring": "npm:^22.0.1"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^25.4.0"
+ "@metamask/keyring-controller": "npm:^26.0.0"
"@metamask/keyring-internal-api": "npm:^11.0.1"
- "@metamask/keyring-utils": "npm:^3.1.0"
+ "@metamask/keyring-sdk": "npm:^2.1.1"
+ "@metamask/keyring-utils": "npm:^3.2.1"
"@metamask/messenger": "npm:^1.2.0"
- "@metamask/network-controller": "npm:^30.1.0"
+ "@metamask/network-controller": "npm:^32.0.0"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^11.9.0"
deepmerge: "npm:^4.2.2"
@@ -7810,7 +7811,7 @@ __metadata:
peerDependencies:
"@metamask/providers": ^22.0.0
webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/29a03bf38972dce690c11ce21a6d68870d3a18b9548647cd3969a097ba37f64c8dea3464bb0bbe56a633de24e30cdb80890dc2cbcc4e4ddbaef28b5dae242c68
+ checksum: 10/1cdaae1818af96e1cccfb95452a54a936299f6aadc1f8f39a5651dbe409eced0379bcd959c2e944caad3fbba91a3b7c7e4a954a8a1eb9804302bfa0336ad7ac6
languageName: node
linkType: hard
@@ -7866,59 +7867,59 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/approval-controller@npm:^9.0.0, @metamask/approval-controller@npm:^9.0.1":
- version: 9.0.1
- resolution: "@metamask/approval-controller@npm:9.0.1"
+"@metamask/approval-controller@npm:^9.0.0, @metamask/approval-controller@npm:^9.0.1, @metamask/approval-controller@npm:^9.0.2":
+ version: 9.0.2
+ resolution: "@metamask/approval-controller@npm:9.0.2"
dependencies:
- "@metamask/base-controller": "npm:^9.0.1"
- "@metamask/messenger": "npm:^1.0.0"
+ "@metamask/base-controller": "npm:^9.1.0"
+ "@metamask/messenger": "npm:^1.2.0"
"@metamask/rpc-errors": "npm:^7.0.2"
"@metamask/utils": "npm:^11.9.0"
nanoid: "npm:^3.3.8"
- checksum: 10/980e7ded7022a887c11693226922f9814d160c93fe5297380addafebe9b6e9191ba3acc7bf54775c8c8eeb7e07bcfcaaf79cc90361ff18fa04c1d449eab2ed33
+ checksum: 10/f27d75901a1a8367a8972b30ebb097a063282c8eb40f3188a6255ed1e043313085de6d5feb9300660366d4d93c822ebe6b37219978806db0492c4fed761d3c6a
languageName: node
linkType: hard
-"@metamask/assets-controller@npm:^8.3.1, @metamask/assets-controller@npm:^8.3.2":
- version: 8.3.2
- resolution: "@metamask/assets-controller@npm:8.3.2"
+"@metamask/assets-controller@npm:^8.3.1, @metamask/assets-controller@npm:^8.3.2, @metamask/assets-controller@npm:^8.3.3":
+ version: 8.3.3
+ resolution: "@metamask/assets-controller@npm:8.3.3"
dependencies:
"@ethereumjs/util": "npm:^9.1.0"
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
- "@metamask/account-tree-controller": "npm:^7.5.1"
- "@metamask/accounts-controller": "npm:^39.0.0"
- "@metamask/assets-controllers": "npm:^108.5.0"
+ "@metamask/account-tree-controller": "npm:^7.5.2"
+ "@metamask/accounts-controller": "npm:^39.0.1"
+ "@metamask/assets-controllers": "npm:^108.6.0"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/client-controller": "npm:^1.0.1"
- "@metamask/controller-utils": "npm:^12.1.0"
- "@metamask/core-backend": "npm:^6.3.2"
+ "@metamask/controller-utils": "npm:^12.1.1"
+ "@metamask/core-backend": "npm:^6.3.3"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/keyring-internal-api": "npm:^11.0.1"
"@metamask/keyring-snap-client": "npm:^9.0.2"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/network-controller": "npm:^32.0.0"
- "@metamask/network-enablement-controller": "npm:^5.2.0"
+ "@metamask/network-enablement-controller": "npm:^5.3.0"
"@metamask/permission-controller": "npm:^13.1.1"
"@metamask/phishing-controller": "npm:^17.2.0"
"@metamask/polling-controller": "npm:^16.0.6"
"@metamask/preferences-controller": "npm:^23.1.0"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/snaps-utils": "npm:^12.1.2"
- "@metamask/transaction-controller": "npm:^66.0.1"
+ "@metamask/transaction-controller": "npm:^67.0.0"
"@metamask/utils": "npm:^11.9.0"
async-mutex: "npm:^0.5.0"
bignumber.js: "npm:^9.1.2"
lodash: "npm:^4.17.21"
p-limit: "npm:^3.1.0"
- checksum: 10/8a1fac3cb15503059ef297b9e4868e38d4571297017745d2341413389c49a1fd8b24c8c3a4f55e82be3485c2a1bb7e8e36bb5f93b6d44f19e29e7ade89584535
+ checksum: 10/aaa1414447aa37913d1a97faf4ea8fc999222346253025caa8a5599e4ed6fd354cb38fd7bca33b23431f0b43ab884b00d4ea6e98c1d94a5cd46b8c01aa14f8c1
languageName: node
linkType: hard
-"@metamask/assets-controllers@npm:^108.4.0, @metamask/assets-controllers@npm:^108.5.0":
- version: 108.5.0
- resolution: "@metamask/assets-controllers@npm:108.5.0"
+"@metamask/assets-controllers@npm:^108.4.0, @metamask/assets-controllers@npm:^108.5.0, @metamask/assets-controllers@npm:^108.6.0":
+ version: 108.6.0
+ resolution: "@metamask/assets-controllers@npm:108.6.0"
dependencies:
"@ethereumjs/util": "npm:^9.1.0"
"@ethersproject/abi": "npm:^5.7.0"
@@ -7927,21 +7928,21 @@ __metadata:
"@ethersproject/contracts": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
"@metamask/abi-utils": "npm:^2.0.3"
- "@metamask/account-tree-controller": "npm:^7.5.1"
- "@metamask/accounts-controller": "npm:^39.0.0"
- "@metamask/approval-controller": "npm:^9.0.1"
+ "@metamask/account-tree-controller": "npm:^7.5.2"
+ "@metamask/accounts-controller": "npm:^39.0.1"
+ "@metamask/approval-controller": "npm:^9.0.2"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/contract-metadata": "npm:^2.4.0"
- "@metamask/controller-utils": "npm:^12.1.0"
- "@metamask/core-backend": "npm:^6.3.2"
+ "@metamask/controller-utils": "npm:^12.1.1"
+ "@metamask/core-backend": "npm:^6.3.3"
"@metamask/eth-query": "npm:^4.0.0"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/metamask-eth-abis": "npm:^3.1.1"
- "@metamask/multichain-account-service": "npm:^10.0.2"
+ "@metamask/multichain-account-service": "npm:^10.0.3"
"@metamask/network-controller": "npm:^32.0.0"
- "@metamask/network-enablement-controller": "npm:^5.2.0"
+ "@metamask/network-enablement-controller": "npm:^5.3.0"
"@metamask/permission-controller": "npm:^13.1.1"
"@metamask/phishing-controller": "npm:^17.2.0"
"@metamask/polling-controller": "npm:^16.0.6"
@@ -7951,8 +7952,8 @@ __metadata:
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/snaps-sdk": "npm:^11.0.0"
"@metamask/snaps-utils": "npm:^12.1.2"
- "@metamask/storage-service": "npm:^1.0.1"
- "@metamask/transaction-controller": "npm:^66.0.1"
+ "@metamask/storage-service": "npm:^1.0.2"
+ "@metamask/transaction-controller": "npm:^67.0.0"
"@metamask/utils": "npm:^11.9.0"
"@tanstack/query-core": "npm:^5.62.16"
"@types/bn.js": "npm:^5.1.5"
@@ -7969,7 +7970,7 @@ __metadata:
peerDependencies:
"@metamask/providers": ^22.0.0
webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/724c5b0a15d74fb3e28842003c4806a287b3fc31acd68dd40b6fb3eda30e70150d35dc391bd3690c4a0b413a4f215c77af04446bb8f2a348952224bc85541938
+ checksum: 10/562b935afa642af263115870b2bd69ae726223c8b84381a0c78c7e154bc59159ee0b68a06e1edabb1f88669e359369eebd9bf7d9e3b0ab9c50334a4a4d5ca01d
languageName: node
linkType: hard
@@ -8081,9 +8082,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/bridge-controller@npm:^73.2.1":
- version: 73.2.1
- resolution: "@metamask/bridge-controller@npm:73.2.1"
+"@metamask/bridge-controller@npm:^74.0.0":
+ version: 74.0.0
+ resolution: "@metamask/bridge-controller@npm:74.0.0"
dependencies:
"@ethersproject/address": "npm:^5.7.0"
"@ethersproject/bignumber": "npm:^5.7.0"
@@ -8105,29 +8106,29 @@ __metadata:
"@metamask/profile-sync-controller": "npm:^28.1.1"
"@metamask/remote-feature-flag-controller": "npm:^4.2.2"
"@metamask/snaps-controllers": "npm:^19.0.0"
- "@metamask/transaction-controller": "npm:^66.0.1"
+ "@metamask/transaction-controller": "npm:^67.0.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
reselect: "npm:^5.1.1"
uuid: "npm:^8.3.2"
- checksum: 10/d1a540ace86ed9226ae5f02a65b6f00ab97d22a82fa14d766e2bebe9e4cc14ba6cbc36d965636916e136a17a9c85aa22ccb286425a760c9327de429230704a50
+ checksum: 10/00f9f88567a0f43b1694bfd2becaef6036fc7fbdd0ad11590e0727d9e0163db241f354fd3dd9f22d731111be32b67a4e3c6e8f6d45b14dcaf1522110fc66f481
languageName: node
linkType: hard
-"@metamask/bridge-controller@npm:^74.0.0":
- version: 74.0.0
- resolution: "@metamask/bridge-controller@npm:74.0.0"
+"@metamask/bridge-controller@npm:^75.0.0, @metamask/bridge-controller@npm:^75.1.0":
+ version: 75.1.0
+ resolution: "@metamask/bridge-controller@npm:75.1.0"
dependencies:
"@ethersproject/address": "npm:^5.7.0"
"@ethersproject/bignumber": "npm:^5.7.0"
"@ethersproject/constants": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
- "@metamask/accounts-controller": "npm:^39.0.0"
- "@metamask/assets-controller": "npm:^8.3.2"
- "@metamask/assets-controllers": "npm:^108.5.0"
+ "@metamask/accounts-controller": "npm:^39.0.1"
+ "@metamask/assets-controller": "npm:^8.3.3"
+ "@metamask/assets-controllers": "npm:^108.6.0"
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/controller-utils": "npm:^12.1.1"
"@metamask/gas-fee-controller": "npm:^26.2.2"
"@metamask/keyring-api": "npm:^23.1.0"
"@metamask/messenger": "npm:^1.2.0"
@@ -8138,36 +8139,36 @@ __metadata:
"@metamask/profile-sync-controller": "npm:^28.1.1"
"@metamask/remote-feature-flag-controller": "npm:^4.2.2"
"@metamask/snaps-controllers": "npm:^19.0.0"
- "@metamask/transaction-controller": "npm:^67.0.0"
+ "@metamask/transaction-controller": "npm:^67.1.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
reselect: "npm:^5.1.1"
uuid: "npm:^8.3.2"
- checksum: 10/00f9f88567a0f43b1694bfd2becaef6036fc7fbdd0ad11590e0727d9e0163db241f354fd3dd9f22d731111be32b67a4e3c6e8f6d45b14dcaf1522110fc66f481
+ checksum: 10/bc4c2ab91337cfc2a277b9dbe3d02bd6600af2f174fd157e68c69e07839cb34d0b7fe6ccdf903535a3265380b4641c48a28b4a7e1dc489629ac8ae6e9b1b5a7f
languageName: node
linkType: hard
-"@metamask/bridge-status-controller@npm:^72.0.2":
- version: 72.0.2
- resolution: "@metamask/bridge-status-controller@npm:72.0.2"
+"@metamask/bridge-status-controller@npm:^72.0.2, @metamask/bridge-status-controller@npm:^72.1.0":
+ version: 72.1.0
+ resolution: "@metamask/bridge-status-controller@npm:72.1.0"
dependencies:
- "@metamask/accounts-controller": "npm:^39.0.0"
+ "@metamask/accounts-controller": "npm:^39.0.1"
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/bridge-controller": "npm:^73.2.1"
- "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/bridge-controller": "npm:^75.0.0"
+ "@metamask/controller-utils": "npm:^12.1.1"
"@metamask/gas-fee-controller": "npm:^26.2.2"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/network-controller": "npm:^32.0.0"
"@metamask/polling-controller": "npm:^16.0.6"
"@metamask/profile-sync-controller": "npm:^28.1.1"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/superstruct": "npm:^3.1.0"
- "@metamask/transaction-controller": "npm:^66.0.1"
+ "@metamask/transaction-controller": "npm:^67.1.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
uuid: "npm:^8.3.2"
- checksum: 10/60f2849e351bfa39c60f18bf30056e0720ad39fce4982a70bdf85b71e85a7da71e9174ea93c3bbf69fa65618ae27ea9fc55ea0e9cfae14b79b7445509e962260
+ checksum: 10/61b940736a8ce327de7e6bc87ed02a5256b0ad7fd6b3c2118d82592d3019b2c134b1779acdb4ea841df4d1b4f75a4046deb7157563223e98ac74b02d08696fa9
languageName: node
linkType: hard
@@ -8296,9 +8297,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0":
- version: 12.1.0
- resolution: "@metamask/controller-utils@npm:12.1.0"
+"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0, @metamask/controller-utils@npm:^12.1.1":
+ version: 12.1.1
+ resolution: "@metamask/controller-utils@npm:12.1.1"
dependencies:
"@metamask/eth-query": "npm:^4.0.0"
"@metamask/ethjs-unit": "npm:^0.3.0"
@@ -8313,24 +8314,24 @@ __metadata:
lodash: "npm:^4.17.21"
peerDependencies:
"@babel/runtime": ^7.0.0
- checksum: 10/421b9685faefa481529e611a11cf005cd94ced8f4a15a6ea1aacd885417be0de6191ee414e0bcb0fff7ecade9e356a4d0f3d7c826e798ba47d65e412aa1df049
+ checksum: 10/39e22e0247ee26a83316563c35ec206053bf95310e66441a6d416f33837f62da97abea1d52429b208e22b23fc97ad13b8a6d93d94607638b0567a45efdab1933
languageName: node
linkType: hard
-"@metamask/core-backend@npm:^6.3.0, @metamask/core-backend@npm:^6.3.2":
- version: 6.3.2
- resolution: "@metamask/core-backend@npm:6.3.2"
+"@metamask/core-backend@npm:^6.3.0, @metamask/core-backend@npm:^6.3.2, @metamask/core-backend@npm:^6.3.3":
+ version: 6.3.3
+ resolution: "@metamask/core-backend@npm:6.3.3"
dependencies:
- "@metamask/accounts-controller": "npm:^39.0.0"
- "@metamask/controller-utils": "npm:^12.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/accounts-controller": "npm:^39.0.1"
+ "@metamask/controller-utils": "npm:^12.1.1"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/profile-sync-controller": "npm:^28.1.1"
"@metamask/utils": "npm:^11.9.0"
"@tanstack/query-core": "npm:^5.62.16"
async-mutex: "npm:^0.5.0"
uuid: "npm:^8.3.2"
- checksum: 10/97965ba67fc3574b857a8f18b66450e141d3ee58afe0a2788d08e55253183692421373c0c076ae75e5763f5206eb7be1af4235d4ab480881a9b80fcec6538541
+ checksum: 10/be94b9131e1babea048a60f058c345d42ea8423fc3a7121013be4713f7d6298a2b9e3d2c454d0a8ddfd21216f3520bf517556b9d7b5b16600f700bf7cb5d98b6
languageName: node
linkType: hard
@@ -8389,9 +8390,9 @@ __metadata:
linkType: hard
"@metamask/delegation-deployments@npm:^1.0.0, @metamask/delegation-deployments@npm:^1.3.0":
- version: 1.3.0
- resolution: "@metamask/delegation-deployments@npm:1.3.0"
- checksum: 10/58f4aafb5f0e3cbc543811cbc0100efab4ed67b9c9794b83192962153e4edbe12fd6ab6fa7be689503309862a65eb7fde771f632893d38ab54f8171aa682b34f
+ version: 1.4.0
+ resolution: "@metamask/delegation-deployments@npm:1.4.0"
+ checksum: 10/e5e7b83e27daec5b1b61482647d43d4b685954818ff02687e2dbe8169a4dfe199cc8d2ed444242b60425ac2e7d2cf2a5ca29f3e69936abe6a8391f578ec693bb
languageName: node
linkType: hard
@@ -8464,17 +8465,17 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/eip-5792-middleware@npm:^2.0.0":
- version: 2.0.0
- resolution: "@metamask/eip-5792-middleware@npm:2.0.0"
+"@metamask/eip-5792-middleware@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@metamask/eip-5792-middleware@npm:3.0.4"
dependencies:
- "@metamask/messenger": "npm:^0.3.0"
+ "@metamask/messenger": "npm:^1.2.0"
"@metamask/superstruct": "npm:^3.1.0"
- "@metamask/transaction-controller": "npm:^61.0.0"
- "@metamask/utils": "npm:^11.8.1"
+ "@metamask/transaction-controller": "npm:^65.4.0"
+ "@metamask/utils": "npm:^11.9.0"
lodash: "npm:^4.17.21"
uuid: "npm:^8.3.2"
- checksum: 10/7ccf426cbf9921b9c079da6877238fafa16353ff0b57027a3615ae9e07a39c1364c96c139673c7ed00dd00efe53e7467dfa9b3cc6c491ae89f68be0dfd047560
+ checksum: 10/cac71793d2379399d964c02c1a8b5907aea177af938e8a18cbcf51a689a2d90736784bef751529a90c67e348382fc8b88116db1a601c41e5797f5b5166640562
languageName: node
linkType: hard
@@ -8710,29 +8711,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/eth-snap-keyring@npm:^19.0.0":
- version: 19.0.0
- resolution: "@metamask/eth-snap-keyring@npm:19.0.0"
- dependencies:
- "@ethereumjs/tx": "npm:^5.4.0"
- "@metamask/eth-sig-util": "npm:^8.2.0"
- "@metamask/keyring-api": "npm:^21.4.0"
- "@metamask/keyring-internal-api": "npm:^10.0.0"
- "@metamask/keyring-internal-snap-client": "npm:^9.0.0"
- "@metamask/keyring-snap-sdk": "npm:^7.2.0"
- "@metamask/keyring-utils": "npm:^3.2.0"
- "@metamask/messenger": "npm:^0.3.0"
- "@metamask/superstruct": "npm:^3.1.0"
- "@metamask/utils": "npm:^11.1.0"
- "@types/uuid": "npm:^9.0.8"
- async-mutex: "npm:^0.5.0"
- uuid: "npm:^9.0.1"
- peerDependencies:
- "@metamask/keyring-api": ^21.4.0
- checksum: 10/6e307295cb15ab44aba4ff89fb1886ad8c0ea6636748e5d84c87250fbaff9d5a3c316e63b614d7b472e4027aad0a7912109b36981e978e978bf31deea7980726
- languageName: node
- linkType: hard
-
"@metamask/eth-snap-keyring@npm:^22.0.1":
version: 22.0.1
resolution: "@metamask/eth-snap-keyring@npm:22.0.1"
@@ -8982,7 +8960,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/keyring-api@npm:23.1.0, @metamask/keyring-api@npm:^23.1.0":
+"@metamask/keyring-api@npm:^23.1.0":
version: 23.1.0
resolution: "@metamask/keyring-api@npm:23.1.0"
dependencies:
@@ -9041,19 +9019,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/keyring-internal-snap-client@npm:^9.0.0":
- version: 9.0.1
- resolution: "@metamask/keyring-internal-snap-client@npm:9.0.1"
- dependencies:
- "@metamask/keyring-api": "npm:^22.0.0"
- "@metamask/keyring-internal-api": "npm:^10.0.1"
- "@metamask/keyring-snap-client": "npm:^8.2.1"
- "@metamask/keyring-utils": "npm:^3.2.0"
- "@metamask/messenger": "npm:^0.3.0"
- checksum: 10/fb683a7826856612d2d89a8f6eb83bd14a20e27c18dddb5c50699d0fefce7475ff9e3077b65296820cf0911ee49d4daf3014932d1b1d7bfd330612229e02e6f4
- languageName: node
- linkType: hard
-
"@metamask/keyring-sdk@npm:^2.0.2, @metamask/keyring-sdk@npm:^2.1.1":
version: 2.1.1
resolution: "@metamask/keyring-sdk@npm:2.1.1"
@@ -9072,22 +9037,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/keyring-snap-client@npm:^8.2.0, @metamask/keyring-snap-client@npm:^8.2.1":
- version: 8.2.1
- resolution: "@metamask/keyring-snap-client@npm:8.2.1"
- dependencies:
- "@metamask/keyring-api": "npm:^22.0.0"
- "@metamask/keyring-utils": "npm:^3.2.0"
- "@metamask/superstruct": "npm:^3.1.0"
- "@types/uuid": "npm:^9.0.8"
- uuid: "npm:^9.0.1"
- webextension-polyfill: "npm:^0.12.0"
- peerDependencies:
- "@metamask/providers": ^19.0.0
- checksum: 10/f69dcb56d3089dfd1bd902ad409c017288e31d3631a2024f42f6b6ee1b4f6e663ad4d7bdfd5d3bcf6c9827190d3bad6983c89f7c821959d305f6ba8f4aae0d7c
- languageName: node
- linkType: hard
-
"@metamask/keyring-snap-client@npm:^9.0.2":
version: 9.0.2
resolution: "@metamask/keyring-snap-client@npm:9.0.2"
@@ -9104,22 +9053,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/keyring-snap-sdk@npm:^7.2.0":
- version: 7.2.0
- resolution: "@metamask/keyring-snap-sdk@npm:7.2.0"
- dependencies:
- "@metamask/keyring-utils": "npm:^3.2.0"
- "@metamask/snaps-sdk": "npm:^10.4.0"
- "@metamask/superstruct": "npm:^3.1.0"
- "@metamask/utils": "npm:^11.1.0"
- webextension-polyfill: "npm:^0.12.0"
- peerDependencies:
- "@metamask/keyring-api": ^21.4.0
- "@metamask/providers": ^19.0.0
- checksum: 10/e805566d60bef72efb7298e9ee05d23acbd6e77e2cc17d47b6e9d09854f1a029b5873754d3ab0a5631a468267c0f21607de1c2cb37f57522a62cabca34b99b4d
- languageName: node
- linkType: hard
-
"@metamask/keyring-snap-sdk@npm:^9.0.1":
version: 9.0.1
resolution: "@metamask/keyring-snap-sdk@npm:9.0.1"
@@ -9136,7 +9069,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/keyring-utils@npm:^3.1.0, @metamask/keyring-utils@npm:^3.2.0, @metamask/keyring-utils@npm:^3.2.1, @metamask/keyring-utils@npm:^3.3.1":
+"@metamask/keyring-utils@npm:^3.2.0, @metamask/keyring-utils@npm:^3.2.1, @metamask/keyring-utils@npm:^3.3.1":
version: 3.3.1
resolution: "@metamask/keyring-utils@npm:3.3.1"
dependencies:
@@ -9285,22 +9218,22 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/multichain-account-service@npm:^10.0.2":
- version: 10.0.2
- resolution: "@metamask/multichain-account-service@npm:10.0.2"
+"@metamask/multichain-account-service@npm:^10.0.2, @metamask/multichain-account-service@npm:^10.0.3":
+ version: 10.0.3
+ resolution: "@metamask/multichain-account-service@npm:10.0.3"
dependencies:
"@ethereumjs/util": "npm:^9.1.0"
- "@metamask/accounts-controller": "npm:^39.0.0"
+ "@metamask/accounts-controller": "npm:^39.0.1"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/eth-snap-keyring": "npm:^22.0.1"
"@metamask/key-tree": "npm:^10.1.1"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/keyring-internal-api": "npm:^11.0.1"
"@metamask/keyring-snap-client": "npm:^9.0.2"
"@metamask/keyring-utils": "npm:^3.2.1"
"@metamask/messenger": "npm:^1.2.0"
- "@metamask/snap-account-service": "npm:^0.3.0"
+ "@metamask/snap-account-service": "npm:^0.3.1"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/snaps-sdk": "npm:^11.0.0"
"@metamask/snaps-utils": "npm:^12.1.2"
@@ -9312,37 +9245,7 @@ __metadata:
"@metamask/account-api": ^1.0.4
"@metamask/providers": ^22.0.0
webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/14ed586988071cbfca53b6d5d263b391f1818e762eb1af175da33ddca38020eb82cdfcded3578093192c891e84ab7232112e8129cd2f98f10cce2d8c1f8d4b48
- languageName: node
- linkType: hard
-
-"@metamask/multichain-account-service@npm:^8.0.1":
- version: 8.0.1
- resolution: "@metamask/multichain-account-service@npm:8.0.1"
- dependencies:
- "@ethereumjs/util": "npm:^9.1.0"
- "@metamask/accounts-controller": "npm:^37.1.1"
- "@metamask/base-controller": "npm:^9.0.1"
- "@metamask/eth-snap-keyring": "npm:^19.0.0"
- "@metamask/key-tree": "npm:^10.1.1"
- "@metamask/keyring-api": "npm:^21.6.0"
- "@metamask/keyring-controller": "npm:^25.1.1"
- "@metamask/keyring-internal-api": "npm:^10.0.0"
- "@metamask/keyring-snap-client": "npm:^8.2.0"
- "@metamask/keyring-utils": "npm:^3.1.0"
- "@metamask/messenger": "npm:^1.0.0"
- "@metamask/snaps-controllers": "npm:^19.0.0"
- "@metamask/snaps-sdk": "npm:^11.0.0"
- "@metamask/snaps-utils": "npm:^12.1.2"
- "@metamask/superstruct": "npm:^3.1.0"
- "@metamask/utils": "npm:^11.9.0"
- async-mutex: "npm:^0.5.0"
- lodash: "npm:^4.17.21"
- peerDependencies:
- "@metamask/account-api": ^1.0.0
- "@metamask/providers": ^22.0.0
- webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/7ac3c38db8afd47593cd7ed4cc95de99195ccd2b2903281382be83990b2a47fa2f03de77e325c1ea3c7fd96367e3eac20039994d95154d478bebacd61215140a
+ checksum: 10/ed945f17e571b96baab959c29e5bdc68573de6e613d79834fbd9eb55025e7c85d3e92ae4cf8454ccefdb45e6331ecfe9f859feb4217cbe1b3083086166128057
languageName: node
linkType: hard
@@ -9454,7 +9357,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/network-enablement-controller@npm:^5.2.0, @metamask/network-enablement-controller@npm:^5.3.0":
+"@metamask/network-enablement-controller@npm:^5.3.0":
version: 5.3.0
resolution: "@metamask/network-enablement-controller@npm:5.3.0"
dependencies:
@@ -10057,22 +9960,22 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/snap-account-service@npm:^0.3.0":
- version: 0.3.0
- resolution: "@metamask/snap-account-service@npm:0.3.0"
+"@metamask/snap-account-service@npm:^0.3.0, @metamask/snap-account-service@npm:^0.3.1":
+ version: 0.3.1
+ resolution: "@metamask/snap-account-service@npm:0.3.1"
dependencies:
"@metamask/account-api": "npm:^1.0.4"
- "@metamask/account-tree-controller": "npm:^7.5.1"
+ "@metamask/account-tree-controller": "npm:^7.5.2"
"@metamask/eth-snap-keyring": "npm:^22.0.1"
"@metamask/keyring-api": "npm:^23.1.0"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/keyring-snap-sdk": "npm:^9.0.1"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/snaps-sdk": "npm:^11.0.0"
"@metamask/utils": "npm:^11.9.0"
lodash: "npm:^4.17.21"
- checksum: 10/b7088073cab6d3c208c33f16b14e7a2a047152efa1c432dd03702e0f4c67c8ca1fe40111f01ccfd6f77052fe9a8d79957a51a1d8272f27dd5ffa3e1d9b6cdc1d
+ checksum: 10/de442502dd59910e991dd57b0c107bdc8fd5b027a2190350f2d6a1a68913f0e65d0e46a3f3cc47aee8a824c5e6dc8062a9f5ecadb4f8b62b92efe71cb4404094
languageName: node
linkType: hard
@@ -10313,13 +10216,13 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/storage-service@npm:^1.0.0, @metamask/storage-service@npm:^1.0.1":
- version: 1.0.1
- resolution: "@metamask/storage-service@npm:1.0.1"
+"@metamask/storage-service@npm:^1.0.0, @metamask/storage-service@npm:^1.0.1, @metamask/storage-service@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "@metamask/storage-service@npm:1.0.2"
dependencies:
- "@metamask/messenger": "npm:^1.0.0"
+ "@metamask/messenger": "npm:^1.2.0"
"@metamask/utils": "npm:^11.9.0"
- checksum: 10/3ec18b85ae80d13c4928be327abb1ee0548a6c44afdb7f709434a6621c876c3de95e145ca2603bdf178772982c76f546ec1cac58f28c0a9c74e020342d171349
+ checksum: 10/dda8f1a6c63c98bbd8fa61cd5863e54f8e9801389a96efc69fd8d10011cd194370990bac5b8693c7861e23769237e617946e0304a1dc9ea4da83b8d3fb6c8942
languageName: node
linkType: hard
@@ -10427,33 +10330,33 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/transaction-pay-controller@npm:^23.3.0":
- version: 23.3.0
- resolution: "@metamask/transaction-pay-controller@npm:23.3.0"
+"@metamask/transaction-pay-controller@npm:^23.5.0":
+ version: 23.5.0
+ resolution: "@metamask/transaction-pay-controller@npm:23.5.0"
dependencies:
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
"@ethersproject/providers": "npm:^5.7.0"
- "@metamask/assets-controller": "npm:^8.3.2"
- "@metamask/assets-controllers": "npm:^108.5.0"
+ "@metamask/assets-controller": "npm:^8.3.3"
+ "@metamask/assets-controllers": "npm:^108.6.0"
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/bridge-controller": "npm:^73.2.1"
- "@metamask/bridge-status-controller": "npm:^72.0.2"
- "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/bridge-controller": "npm:^75.1.0"
+ "@metamask/bridge-status-controller": "npm:^72.1.0"
+ "@metamask/controller-utils": "npm:^12.1.1"
"@metamask/gas-fee-controller": "npm:^26.2.2"
- "@metamask/keyring-controller": "npm:^26.0.0"
+ "@metamask/keyring-controller": "npm:^27.0.0"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/metamask-eth-abis": "npm:^3.1.1"
"@metamask/network-controller": "npm:^32.0.0"
"@metamask/ramps-controller": "npm:^14.1.1"
"@metamask/remote-feature-flag-controller": "npm:^4.2.2"
- "@metamask/transaction-controller": "npm:^67.0.0"
+ "@metamask/transaction-controller": "npm:^67.1.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
bn.js: "npm:^5.2.1"
immer: "npm:^9.0.6"
lodash: "npm:^4.17.21"
- checksum: 10/2e3a89c5788c2c335740a0c2bf312c7ad28ceb8109acf07c6fdd9ce80c99f88c137dc5f0917d0434855fe58c8ccb5cd83184c1d6b0976ce4faeeedc351a5f1e0
+ checksum: 10/fee0488d37c030298b29b6b8369a14bb2ff925c3c2cd1e555e8ee6236fecf040a4aaaaef6767dcca96e8154846ea7262ebc2bfc863c58657952245571a0391d0
languageName: node
linkType: hard
@@ -10464,7 +10367,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/utils@npm:^11.0.0, @metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.11.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.5.0, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0":
+"@metamask/utils@npm:^11.0.0, @metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.11.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.5.0, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0":
version: 11.11.0
resolution: "@metamask/utils@npm:11.11.0"
dependencies:
@@ -35327,8 +35230,8 @@ __metadata:
"@ledgerhq/react-native-hw-transport-ble": "npm:^6.37.0"
"@metamask/abi-utils": "npm:^3.0.0"
"@metamask/account-api": "npm:^1.0.4"
- "@metamask/account-tree-controller": "npm:^7.2.0"
- "@metamask/accounts-controller": "npm:^38.0.0"
+ "@metamask/account-tree-controller": "npm:^7.5.0"
+ "@metamask/accounts-controller": "npm:^39.0.0"
"@metamask/address-book-controller": "npm:^7.1.2"
"@metamask/ai-controllers": "npm:^0.7.0"
"@metamask/analytics-controller": "npm:^1.0.0"
@@ -35358,7 +35261,7 @@ __metadata:
"@metamask/design-system-twrnc-preset": "npm:^0.5.0"
"@metamask/design-tokens": "npm:^8.5.0"
"@metamask/earn-controller": "npm:^12.1.0"
- "@metamask/eip-5792-middleware": "npm:^2.0.0"
+ "@metamask/eip-5792-middleware": "npm:^3.0.4"
"@metamask/eip1193-permission-middleware": "npm:^1.0.2"
"@metamask/ens-resolver-snap": "npm:^1.2.0"
"@metamask/eslint-config-typescript": "npm:^13.0.0"
@@ -35387,9 +35290,8 @@ __metadata:
"@metamask/keyring-api": "npm:^23.1.0"
"@metamask/keyring-controller": "npm:^26.0.0"
"@metamask/keyring-internal-api": "npm:^11.0.1"
- "@metamask/keyring-sdk": "npm:^2.0.2"
+ "@metamask/keyring-sdk": "npm:^2.1.1"
"@metamask/keyring-snap-client": "npm:^9.0.2"
- "@metamask/keyring-utils": "npm:^3.2.0"
"@metamask/logging-controller": "npm:^8.0.0"
"@metamask/message-signing-snap": "npm:^1.1.2"
"@metamask/messenger": "npm:^1.2.0"
@@ -35400,7 +35302,7 @@ __metadata:
"@metamask/money-account-balance-service": "npm:^1.0.0"
"@metamask/money-account-controller": "npm:^0.2.0"
"@metamask/money-account-upgrade-controller": "npm:^2.0.0"
- "@metamask/multichain-account-service": "npm:^8.0.1"
+ "@metamask/multichain-account-service": "npm:^10.0.2"
"@metamask/multichain-api-client": "npm:^0.10.1"
"@metamask/multichain-api-middleware": "npm:^3.1.1"
"@metamask/multichain-network-controller": "npm:^3.1.0"
@@ -35438,6 +35340,7 @@ __metadata:
"@metamask/skills": "npm:^0.1.0"
"@metamask/slip44": "npm:^4.2.0"
"@metamask/smart-transactions-controller": "npm:^24.2.0"
+ "@metamask/snap-account-service": "npm:^0.3.0"
"@metamask/snaps-controllers": "npm:^20.0.5"
"@metamask/snaps-execution-environments": "npm:^11.0.2"
"@metamask/snaps-rpc-methods": "npm:^16.0.0"
@@ -35454,7 +35357,7 @@ __metadata:
"@metamask/test-dapp-multichain": "npm:^0.17.1"
"@metamask/test-dapp-solana": "npm:^0.3.0"
"@metamask/transaction-controller": "npm:^67.0.0"
- "@metamask/transaction-pay-controller": "npm:^23.3.0"
+ "@metamask/transaction-pay-controller": "npm:^23.5.0"
"@metamask/tron-wallet-snap": "npm:^1.25.6"
"@metamask/utils": "npm:^11.11.0"
"@metamask/wallet": "npm:^2.0.0"