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"