From 6c465bc88b31f3e4e618ba34da8160711d261cad Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 25 Apr 2024 15:32:37 +0200 Subject: [PATCH 001/107] fix: network verification for collision network + local network (#23802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** here are the different fixes that this PR contains: collision of two networks with the same chainId but with two different symbols ==> solution: manage this as a special case, add the other network and extend the list https://chainid.network/chains.json scam warning displayed for networks running on localhost ==> exclude from network verification all networks running on localhost. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/23802?quickstart=1) ## **Related issues** Fixes: #23242 ## **Manual testing steps** 1. Go to add network page 2. Go to custom add network form 3. Add the network with chainID 78 and rpc https://rpc.wethio.io/ and symbol ZYN ## **Screenshots/Recordings** ### **Before** Screenshot 2024-03-29 at 10 49 00 Screenshot 2024-03-29 at 10 49 18 ### **After** Screenshot 2024-03-29 at 10 35 41 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/constants/network.ts | 27 ++ .../tests/network/add-custom-network.spec.js | 230 ++++++++++++++++++ ui/components/app/add-network/add-network.js | 1 + ui/components/app/asset-list/asset-list.js | 3 +- .../app/currency-input/currency-input.js | 3 +- .../app/wallet-overview/eth-overview.js | 3 +- ui/helpers/utils/network-helper.test.ts | 79 ++++++ ui/helpers/utils/network-helper.ts | 28 +++ ui/hooks/useIsOriginalNativeTokenSymbol.js | 44 +++- .../useIsOriginalNativeTokenSymbol.test.js | 124 ++++++++++ .../add-network-modal.test.js.snap | 10 + .../networks-form/networks-form.js | 107 +++++--- .../networks-form/networks-form.test.js | 56 +++++ 13 files changed, 677 insertions(+), 38 deletions(-) create mode 100644 ui/helpers/utils/network-helper.test.ts create mode 100644 ui/helpers/utils/network-helper.ts diff --git a/shared/constants/network.ts b/shared/constants/network.ts index abfcfe155e60..eff28ea185ad 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -154,6 +154,8 @@ export const CHAIN_IDS = { POLYGON_ZKEVM: '0x44d', SCROLL: '0x82750', SCROLL_SEPOLIA: '0x8274f', + WETHIO: '0x4e', + CHZ: '0x15b38', } as const; const CHAINLIST_CHAIN_IDS_MAP = { @@ -368,6 +370,11 @@ const CHAINLIST_CURRENCY_SYMBOLS_MAP = { ACALA_NETWORK: 'ACA', } as const; +export const CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION = { + WETHIO: 'ZYN', + CHZ: 'CHZ', +}; + export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg'; export const LINEA_GOERLI_TOKEN_IMAGE_URL = './images/linea-logo-testnet.png'; export const LINEA_SEPOLIA_TOKEN_IMAGE_URL = './images/linea-logo-testnet.png'; @@ -651,6 +658,23 @@ export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { CHAINLIST_CURRENCY_SYMBOLS_MAP.ACALA_NETWORK, } as const; +/** + * A mapping for networks with chain ID collisions to their currencies symbols. + * Useful for networks not listed on https://chainid.network/chains.json due to ID conflicts. + */ +export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION = { + [CHAINLIST_CHAIN_IDS_MAP.CHZ]: [ + { + currencySymbol: CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION.CHZ, + }, + ], + [CHAINLIST_CHAIN_IDS_MAP.WETHIO]: [ + { + currencySymbol: CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION.WETHIO, + }, + ], +}; + export const CHAIN_ID_TO_TYPE_MAP = { [CHAIN_IDS.MAINNET]: NETWORK_TYPES.MAINNET, [CHAIN_IDS.GOERLI]: NETWORK_TYPES.GOERLI, @@ -734,6 +758,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.ZKATANA]: ZKATANA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ZORA_MAINNET]: ZORA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.FILECOIN]: FILECOIN_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.BASE]: BASE_TOKEN_IMAGE_URL, } as const; export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = { @@ -902,6 +927,8 @@ export const BUYABLE_CHAINS_MAP: { | typeof CHAIN_IDS.POLYGON_ZKEVM | typeof CHAIN_IDS.SCROLL | typeof CHAIN_IDS.SCROLL_SEPOLIA + | typeof CHAIN_IDS.WETHIO + | typeof CHAIN_IDS.CHZ >]: BuyableChainSettings; } = { [CHAIN_IDS.MAINNET]: { diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index 183679bc0998..f849c9b0fc12 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -11,6 +11,7 @@ const { } = require('../../helpers'); const TEST_CHAIN_ID = toHex(100); +const TEST_COLLISION_CHAIN_ID = toHex(78); const MOCK_CHAINLIST_RESPONSE = [ { @@ -62,6 +63,61 @@ const MOCK_CHAINLIST_RESPONSE = [ }, ]; +const selectors = { + accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', + informationSymbol: '[data-testid="info-tooltip"]', + settingsOption: { text: 'Settings', tag: 'div' }, + networkOption: { text: 'Networks', tag: 'div' }, + addNetwork: { text: 'Add a network', tag: 'button' }, + addNetworkManually: { text: 'Add a network manually', tag: 'h6' }, + generalOption: { text: 'General', tag: 'div' }, + generalTabHeader: { text: 'General', tag: 'h4' }, + ethereumNetwork: { text: 'Ethereum Mainnet', tag: 'div' }, + newUpdateNetwork: { text: 'Update Network', tag: 'div' }, + deleteButton: { text: 'Delete', tag: 'button' }, + cancelButton: { text: 'Cancel', tag: 'button' }, + saveButton: { text: 'Save', tag: 'button' }, + updatedNetworkDropDown: { tag: 'span', text: 'Update Network' }, + errorMessageInvalidUrl: { + tag: 'h6', + text: 'URLs require the appropriate HTTP/HTTPS prefix.', + }, + warningSymbol: { + tag: 'h6', + text: 'URLs require the appropriate HTTP/HTTPS prefix.', + }, + suggestedTicker: '[data-testid="network-form-ticker-suggestion"]', + tickerWarning: '[data-testid="network-form-ticker-warning"]', + tickerButton: { text: 'PETH', tag: 'button' }, + networkAdded: { text: 'Network added successfully!', tag: 'h4' }, + + networkNameInputField: '[data-testid="network-form-network-name"]', + networkNameInputFieldSetToEthereumMainnet: { + xpath: + "//input[@data-testid = 'network-form-network-name'][@value = 'Ethereum Mainnet']", + }, + rpcUrlInputField: '[data-testid="network-form-rpc-url"]', + chainIdInputField: '[data-testid="network-form-chain-id"]', + tickerInputField: '[data-testid="network-form-ticker-input"]', + explorerInputField: '[data-testid="network-form-block-explorer-url"]', + errorContainer: '.settings-tab__error', +}; + +async function navigateToAddNetwork(driver) { + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.settingsOption); + await driver.clickElement(selectors.networkOption); + await driver.clickElement(selectors.addNetwork); + await driver.clickElement(selectors.addNetworkManually); +} + +const inputData = { + networkName: 'Collision network', + rpcUrl: 'https://responsive-rpc.url/', + chainId: '78', + ticker: 'TST', +}; + describe('Custom network', function () { const chainID = '42161'; const networkURL = 'https://arbitrum-mainnet.infura.io'; @@ -621,6 +677,180 @@ describe('Custom network', function () { ); }); }); + + describe('customNetwork', function () { + it('should add mainnet network', async function () { + async function mockRPCURLAndChainId(mockServer) { + return [ + await mockServer + .forPost('https://responsive-rpc.url/') + .thenCallback(() => ({ + statusCode: 200, + json: { + id: '1694444405781', + jsonrpc: '2.0', + result: TEST_CHAIN_ID, + }, + })), + ]; + } + + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockRPCURLAndChainId, + }, + + async ({ driver }) => { + await unlockWallet(driver); + await navigateToAddNetwork(driver); + await driver.fill( + selectors.networkNameInputField, + 'Ethereum mainnet', + ); + await driver.fill( + selectors.rpcUrlInputField, + 'https://responsive-rpc.url', + ); + await driver.fill(selectors.chainIdInputField, TEST_CHAIN_ID); + await driver.fill(selectors.tickerInputField, 'XDAI'); + await driver.fill(selectors.explorerInputField, 'https://test.com'); + + const suggestedTicker = await driver.isElementPresent( + selectors.suggestedTicker, + ); + + const tickerWarning = await driver.isElementPresent( + selectors.tickerWarning, + ); + + assert.equal(suggestedTicker, false); + assert.equal(tickerWarning, false); + + driver.clickElement(selectors.tickerButton); + driver.clickElement(selectors.saveButton); + + // Validate the network was added + const networkAdded = await driver.isElementPresent( + selectors.networkAdded, + ); + assert.equal(networkAdded, true, 'Network added successfully!'); + }, + ); + }); + + it('should check symbol and show warnings', async function () { + async function mockRPCURLAndChainId(mockServer) { + return [ + await mockServer + .forPost('https://responsive-rpc.url/') + .thenCallback(() => ({ + statusCode: 200, + json: { + id: '1694444405781', + jsonrpc: '2.0', + result: TEST_CHAIN_ID, + }, + })), + ]; + } + + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockRPCURLAndChainId, + }, + + async ({ driver }) => { + await unlockWallet(driver); + await navigateToAddNetwork(driver); + await driver.fill( + selectors.networkNameInputField, + 'Ethereum mainnet', + ); + await driver.fill( + selectors.rpcUrlInputField, + 'https://responsive-rpc.url', + ); + await driver.fill(selectors.chainIdInputField, '1'); + await driver.fill(selectors.tickerInputField, 'TST'); + await driver.fill(selectors.explorerInputField, 'https://test.com'); + + const suggestedTicker = await driver.isElementPresent( + selectors.suggestedTicker, + ); + + const tickerWarning = await driver.isElementPresent( + selectors.tickerWarning, + ); + + // suggestion and warning ticker should be displayed + assert.equal(suggestedTicker, true); + assert.equal(tickerWarning, true); + }, + ); + }); + it('should add collision network', async function () { + async function mockRPCURLAndChainId(mockServer) { + return [ + await mockServer + .forPost('https://responsive-rpc.url/') + .thenCallback(() => ({ + statusCode: 200, + json: { + id: '1694444405781', + jsonrpc: '2.0', + result: TEST_COLLISION_CHAIN_ID, + }, + })), + ]; + } + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockRPCURLAndChainId, + }, + + async ({ driver }) => { + await unlockWallet(driver); + await navigateToAddNetwork(driver); + await driver.fill( + selectors.networkNameInputField, + inputData.networkName, + ); + await driver.fill(selectors.rpcUrlInputField, inputData.rpcUrl); + await driver.fill(selectors.chainIdInputField, inputData.chainId); + await driver.fill(selectors.tickerInputField, inputData.ticker); + + const suggestedTicker = await driver.isElementPresent( + selectors.suggestedTicker, + ); + + const tickerWarning = await driver.isElementPresent( + selectors.tickerWarning, + ); + + assert.equal(suggestedTicker, true); + assert.equal(tickerWarning, true); + + driver.clickElement(selectors.tickerButton); + driver.clickElement(selectors.saveButton); + + // Validate the network was added + const networkAdded = await driver.isElementPresent( + selectors.networkAdded, + ); + assert.equal(networkAdded, true, 'Network added successfully!'); + }, + ); + }); + }); }); async function checkThatSafeChainsListValidationToggleIsOn(driver) { diff --git a/ui/components/app/add-network/add-network.js b/ui/components/app/add-network/add-network.js index 3a96a6b9688f..2d4cd29dc648 100644 --- a/ui/components/app/add-network/add-network.js +++ b/ui/components/app/add-network/add-network.js @@ -160,6 +160,7 @@ const AddNetwork = () => { variant={TextVariant.headingSm} as="h4" color={TextColor.textDefault} + data-testid="add-network-button" > {t('addANetwork')} diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 8e47993eac17..a4e30cad81fc 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -58,11 +58,12 @@ const AssetList = ({ onClickAsset }) => { const { chainId } = useSelector(getCurrentNetwork); const isMainnet = useSelector(getIsMainnet); const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); - const { ticker, type } = useSelector(getProviderConfig); + const { ticker, type, rpcUrl } = useSelector(getProviderConfig); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, type, + rpcUrl, ); const trackEvent = useContext(MetaMetricsContext); const balance = useSelector(getSelectedAccountCachedBalance); diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js index d22ceb289847..f19dfd91d9fa 100644 --- a/ui/components/app/currency-input/currency-input.js +++ b/ui/components/app/currency-input/currency-input.js @@ -68,11 +68,12 @@ export default function CurrencyInput({ const [fiatDecimalValue, setFiatDecimalValue] = useState('0'); const chainId = useSelector(getCurrentChainId); - const { ticker, type } = useSelector(getProviderConfig); + const { ticker, type, rpcUrl } = useSelector(getProviderConfig); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, type, + rpcUrl, ); const tokenToFiatConversionRate = useTokenExchangeRate(asset?.address); diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index abd5351ed712..08d12e1d2958 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -87,12 +87,13 @@ const EthOverview = ({ className, showAddress }) => { const showFiat = useSelector(getShouldShowFiat); const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences); const chainId = useSelector(getCurrentChainId); - const { ticker, type } = useSelector(getProviderConfig); + const { ticker, type, rpcUrl } = useSelector(getProviderConfig); const balance = useSelector(getSelectedAccountCachedBalance); const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol( chainId, ticker, type, + rpcUrl, ); const account = useSelector(getSelectedInternalAccount); diff --git a/ui/helpers/utils/network-helper.test.ts b/ui/helpers/utils/network-helper.test.ts new file mode 100644 index 000000000000..fef6711dbd93 --- /dev/null +++ b/ui/helpers/utils/network-helper.test.ts @@ -0,0 +1,79 @@ +import { getMatchedChain, getMatchedSymbols } from './network-helper'; + +describe('netwotkHelper', () => { + describe('getMatchedChain', () => { + it('should return the matched chain for a given decimalChainId', () => { + const chains = [ + { + chainId: '1', + name: 'Ethereum Mainnet', + nativeCurrency: { symbol: 'ETH' }, + }, + { + chainId: '3', + name: 'Ropsten Testnet', + nativeCurrency: { symbol: 'ETH' }, + }, + ]; + const decimalChainId = '3'; + const expected = { + chainId: '3', + name: 'Ropsten Testnet', + nativeCurrency: { symbol: 'ETH' }, + }; + + const result = getMatchedChain(decimalChainId, chains); + + expect(result).toEqual(expected); + }); + + it('should return undefined if no chain matches the given decimalChainId', () => { + const chains = [ + { + chainId: '1', + name: 'Ethereum Mainnet', + nativeCurrency: { symbol: 'ETH' }, + }, + { + chainId: '3', + name: 'Ropsten Testnet', + nativeCurrency: { symbol: 'ETH' }, + }, + ]; + const decimalChainId = '4'; // No matching chainId + + const result = getMatchedChain(decimalChainId, chains); + + expect(result).toBeUndefined(); + }); + }); + + describe('getMatchedSymbols', () => { + it('should return an array of symbols that match the given decimalChainId', () => { + const chains = [ + { chainId: '1', name: 'test', nativeCurrency: { symbol: 'ETH' } }, + { chainId: '3', name: 'test', nativeCurrency: { symbol: 'tETH' } }, + { chainId: '1', name: 'test', nativeCurrency: { symbol: 'WETH' } }, + ]; + const decimalChainId = '1'; + const expected = ['ETH', 'WETH']; + + const result = getMatchedSymbols(decimalChainId, chains); + + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toBe(expected.length); + }); + + it('should return an empty array if no symbols match the given decimalChainId', () => { + const chains = [ + { chainId: '1', name: 'test', nativeCurrency: { symbol: 'ETH' } }, + { chainId: '3', name: 'test', nativeCurrency: { symbol: 'tETH' } }, + ]; + const decimalChainId = '2'; // No matching chainId + + const result = getMatchedSymbols(decimalChainId, chains); + + expect(result).toEqual([]); + }); + }); +}); diff --git a/ui/helpers/utils/network-helper.ts b/ui/helpers/utils/network-helper.ts new file mode 100644 index 000000000000..840dc8dfbf8a --- /dev/null +++ b/ui/helpers/utils/network-helper.ts @@ -0,0 +1,28 @@ +export const getMatchedChain = ( + decimalChainId: string, + safeChainsList: { + chainId: string; + name: string; + nativeCurrency: { symbol: string }; + }[], +) => { + return safeChainsList.find( + (chain) => chain.chainId.toString() === decimalChainId, + ); +}; + +export const getMatchedSymbols = ( + decimalChainId: string, + safeChainsList: { + chainId: string; + name: string; + nativeCurrency: { symbol: string }; + }[], +): string[] => { + return safeChainsList.reduce((accumulator, currentNetwork) => { + if (currentNetwork.chainId.toString() === decimalChainId) { + accumulator.push(currentNetwork.nativeCurrency?.symbol); + } + return accumulator; + }, []); +}; diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js index 5d6059d736ea..3f885d28c8eb 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js @@ -1,15 +1,34 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import fetchWithCache from '../../shared/lib/fetch-with-cache'; -import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP } from '../../shared/constants/network'; +import { + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION, +} from '../../shared/constants/network'; import { DAY } from '../../shared/constants/time'; import { useSafeChainsListValidationSelector } from '../selectors'; +import { getValidUrl } from '../../app/scripts/lib/util'; -export function useIsOriginalNativeTokenSymbol(chainId, ticker, type) { +export function useIsOriginalNativeTokenSymbol( + chainId, + ticker, + type, + rpcUrl = null, +) { const [isOriginalNativeSymbol, setIsOriginalNativeSymbol] = useState(null); const useSafeChainsListValidation = useSelector( useSafeChainsListValidationSelector, ); + + const isLocalhost = (urlString) => { + const url = getValidUrl(urlString); + + return ( + url !== null && + (url.hostname === 'localhost' || url.hostname === '127.0.0.1') + ); + }; + useEffect(() => { async function getNativeTokenSymbol(networkId) { try { @@ -18,12 +37,32 @@ export function useIsOriginalNativeTokenSymbol(chainId, ticker, type) { return; } + // exclude local dev network + if (isLocalhost(rpcUrl)) { + setIsOriginalNativeSymbol(true); + return; + } + const mappedCurrencySymbol = CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[chainId]; if (mappedCurrencySymbol) { setIsOriginalNativeSymbol(mappedCurrencySymbol === ticker); return; } + const mappedAsNetworkCollision = + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION[chainId]; + + const isMappedCollision = + mappedAsNetworkCollision && + mappedAsNetworkCollision.some( + (network) => network.currencySymbol === ticker, + ); + + if (isMappedCollision) { + setIsOriginalNativeSymbol(true); + return; + } + const safeChainsList = await fetchWithCache({ url: 'https://chainid.network/chains.json', cacheOptions: { cacheRefreshTime: DAY }, @@ -48,6 +87,7 @@ export function useIsOriginalNativeTokenSymbol(chainId, ticker, type) { chainId, ticker, type, + rpcUrl, useSafeChainsListValidation, ]); diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js index 1332eb295d1c..c28070e47077 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.test.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.test.js @@ -196,4 +196,128 @@ describe('useNativeTokenFiatAmount', () => { expect(result.result.current).toBe(true); expect(spyFetch).not.toHaveBeenCalled(); }); + + it('should return false if rpcUrl is localhost', async () => { + useSelector.mockImplementation(generateUseSelectorRouter(true)); + // Mock the fetchWithCache function to throw an error + const spyFetch = jest + .spyOn(fetchWithCacheModule, 'default') + .mockImplementation(() => { + throw new Error('error'); + }); + + let result; + + await act(async () => { + result = renderHook(() => + useIsOriginalNativeTokenSymbol( + '0x13a', + 'FIL', + 'mainnet', + 'http://localhost:8545', + ), + ); + }); + + // Expect the hook to return false when the native symbol does not match the ticker + expect(result.result.current).toBe(true); + expect(spyFetch).not.toHaveBeenCalled(); + }); + + it('should not return false for collision network', async () => { + useSelector.mockImplementation(generateUseSelectorRouter(true)); + // Mock the fetchWithCache function to throw an error + const spyFetch = jest + .spyOn(fetchWithCacheModule, 'default') + .mockImplementation(() => { + throw new Error('error'); + }); + + let result; + + await act(async () => { + result = renderHook(() => + useIsOriginalNativeTokenSymbol( + '0x4e', + 'ZYN', + 'mainnet', + 'https://rpc.wethio.io', + ), + ); + }); + + // Expect the hook to return false when the native symbol does not match the ticker + expect(result.result.current).toBe(true); + expect(spyFetch).not.toHaveBeenCalled(); + }); + + it('should return false for collision network with wrong symbol', async () => { + useSelector.mockImplementation(generateUseSelectorRouter(true)); + // Mock the safeChainsList response + const safeChainsList = [ + { + chainId: 78, + nativeCurrency: { + symbol: 'PETH', + }, + }, + ]; + + // Mock the fetchWithCache function to return the safeChainsList + const spyFetch = jest + .spyOn(fetchWithCacheModule, 'default') + .mockResolvedValue(safeChainsList); + + let result; + + await act(async () => { + result = renderHook(() => + useIsOriginalNativeTokenSymbol( + '0x4e', + 'TEST', + 'mainnet', + 'https://rpc.wethio.io', + ), + ); + }); + + // Expect the hook to return false when the native symbol does not match the ticker + expect(result.result.current).toBe(false); + expect(spyFetch).toHaveBeenCalled(); + }); + + it('should return true for collision network with correct symbol', async () => { + useSelector.mockImplementation(generateUseSelectorRouter(true)); + // Mock the safeChainsList response + const safeChainsList = [ + { + chainId: 78, + nativeCurrency: { + symbol: 'PETH', + }, + }, + ]; + + // Mock the fetchWithCache function to return the safeChainsList + const spyFetch = jest + .spyOn(fetchWithCacheModule, 'default') + .mockResolvedValue(safeChainsList); + + let result; + + await act(async () => { + result = renderHook(() => + useIsOriginalNativeTokenSymbol( + '0x4e', + 'PETH', + 'mainnet', + 'https://rpc.wethio.io', + ), + ); + }); + + // Expect the hook to return false when the native symbol does not match the ticker + expect(result.result.current).toBe(true); + expect(spyFetch).toHaveBeenCalled(); + }); }); diff --git a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap index 9b0bd89d8cfb..ace866477762 100644 --- a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap +++ b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap @@ -175,6 +175,16 @@ exports[`Add Network Modal should render 1`] = ` value="" /> +
+ + Suggested ticker symbol: + +
{ const t = useI18nContext(); const dispatch = useDispatch(); + const DEFAULT_SUGGESTED_TICKER = []; const { label, labelKey, viewOnly, rpcPrefs } = selectedNetwork; const selectedNetworkName = label || (labelKey && t(getNetworkLabelKey(labelKey))); @@ -109,7 +115,9 @@ const NetworksForm = ({ const [rpcUrl, setRpcUrl] = useState(selectedNetwork?.rpcUrl || ''); const [chainId, setChainId] = useState(selectedNetwork?.chainId || ''); const [ticker, setTicker] = useState(selectedNetwork?.ticker || ''); - const [suggestedTicker, setSuggestedTicker] = useState(''); + const [suggestedTicker, setSuggestedTicker] = useState( + DEFAULT_SUGGESTED_TICKER, + ); const [blockExplorerUrl, setBlockExplorerUrl] = useState( selectedNetwork?.blockExplorerUrl || '', ); @@ -145,7 +153,21 @@ const NetworksForm = ({ chainList[index].nativeCurrency.symbol = network.ticker; } }); - safeChainsList.current = chainList; + safeChainsList.current = [ + ...chainList, + { + chainId: 78, + nativeCurrency: { + symbol: CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION.WETHIO, + }, + }, + { + chainId: 88888, + nativeCurrency: { + symbol: CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION.CHZ, + }, + }, + ]; } catch (error) { log.warn('Failed to fetch chainList from chainid.network', error); } @@ -163,7 +185,7 @@ const NetworksForm = ({ setBlockExplorerUrl(selectedNetwork?.blockExplorerUrl); setErrors({}); setWarnings({}); - setSuggestedTicker(''); + setSuggestedTicker([]); setIsSubmitting(false); setIsEditing(false); setPreviousNetwork(selectedNetwork); @@ -261,18 +283,28 @@ const NetworksForm = ({ const autoSuggestTicker = useCallback((formChainId) => { const decimalChainId = getDisplayChainId(formChainId); if (decimalChainId.trim() === '' || safeChainsList.current.length === 0) { - setSuggestedTicker(''); + setSuggestedTicker([]); return; } const matchedChain = safeChainsList.current?.find( (chain) => chain.chainId.toString() === decimalChainId, ); + + const matchedSymbol = safeChainsList.current?.reduce( + (accumulator, currentNetwork) => { + if (currentNetwork.chainId.toString() === decimalChainId) { + accumulator.push(currentNetwork.nativeCurrency?.symbol); + } + return accumulator; + }, + [], + ); + if (matchedChain === undefined) { - setSuggestedTicker(''); + setSuggestedTicker([]); return; } - const returnedTickerSymbol = matchedChain.nativeCurrency?.symbol; - setSuggestedTicker(returnedTickerSymbol); + setSuggestedTicker([...matchedSymbol]); }, []); const hasErrors = () => { @@ -442,23 +474,26 @@ const NetworksForm = ({ warningKey = 'failedToFetchTickerSymbolData'; warningMessage = t('failedToFetchTickerSymbolData'); } else { - const matchedChain = safeChainsList.current?.find( - (chain) => chain.chainId.toString() === decimalChainId, + const matchedChain = getMatchedChain( + decimalChainId, + safeChainsList.current, + ); + const matchedSymbols = getMatchedSymbols( + decimalChainId, + safeChainsList.current, ); if (matchedChain === undefined) { warningKey = 'failedToFetchTickerSymbolData'; warningMessage = t('failedToFetchTickerSymbolData'); - } else { - const returnedTickerSymbol = matchedChain.nativeCurrency?.symbol; - if ( - returnedTickerSymbol.toLowerCase() !== - formTickerSymbol.toLowerCase() - ) { - warningKey = 'chainListReturnedDifferentTickerSymbol'; - warningMessage = t('chainListReturnedDifferentTickerSymbol'); - setSuggestedTicker(returnedTickerSymbol); - } + } else if ( + !matchedSymbols.some( + (symbol) => symbol.toLowerCase() === formTickerSymbol.toLowerCase(), + ) + ) { + warningKey = 'chainListReturnedDifferentTickerSymbol'; + warningMessage = t('chainListReturnedDifferentTickerSymbol'); + setSuggestedTicker([...matchedSymbols]); } } @@ -751,7 +786,10 @@ const NetworksForm = ({ symbolSuggested === ticker, + ) ? ( {t('suggestedTokenSymbol')} - { - setTicker(suggestedTicker); - }} - paddingLeft={1} - paddingRight={1} - style={{ verticalAlign: 'baseline' }} - > - {suggestedTicker} - + {suggestedTicker.map((suggestedSymbol, i) => ( + { + setTicker(suggestedSymbol); + }} + paddingLeft={1} + paddingRight={1} + style={{ verticalAlign: 'baseline' }} + key={i} + > + {suggestedSymbol} + + ))} ) : null } diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.test.js b/ui/pages/settings/networks-tab/networks-form/networks-form.test.js index 5bd701a36e51..6e82265942ac 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.test.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.test.js @@ -337,4 +337,60 @@ describe('NetworkForm Component', () => { await screen.findByTestId('network-form-ticker-warning'), ).not.toBeInTheDocument(); }); + + it('should validate currency symbol field for ZYN network', async () => { + const safeChainsList = [ + { + chainId: 78, + nativeCurrency: { + symbol: 'PETH', + }, + }, + ]; + + // Mock the fetchWithCache function to return the safeChainsList + jest + .spyOn(fetchWithCacheModule, 'default') + .mockResolvedValue(safeChainsList); + + renderComponent(propNewNetwork); + + const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' }); + const currencySymbolField = screen.getByTestId('network-form-ticker-input'); + + fireEvent.change(chainIdField, { + target: { value: '78' }, + }); + + fireEvent.change(currencySymbolField, { + target: { value: 'ZYN' }, + }); + + expect( + await screen.queryByTestId('network-form-ticker-suggestion'), + ).not.toBeInTheDocument(); + + fireEvent.change(currencySymbolField, { + target: { value: 'ETH' }, + }); + + expect( + await screen.queryByTestId('network-form-ticker-suggestion'), + ).toBeInTheDocument(); + + const expectedSymbolWarning = 'Suggested ticker symbol:'; + expect(await screen.findByText(expectedSymbolWarning)).toBeInTheDocument(); + + fireEvent.change(currencySymbolField, { + target: { value: 'PETH' }, + }); + + expect( + await screen.queryByTestId('network-form-ticker-suggestion'), + ).not.toBeInTheDocument(); + + expect( + await screen.queryByText(expectedSymbolWarning), + ).not.toBeInTheDocument(); + }); }); From d5dbf741a3c9dcd5018b32254d39019fde53f1ef Mon Sep 17 00:00:00 2001 From: George Marshall Date: Thu, 25 Apr 2024 08:30:10 -0700 Subject: [PATCH 002/107] chore: adding design tokens eslint plugin `color-no-hex` rule (#24089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces the [@metamask/eslint-plugin-design-tokens](https://github.com/MetaMask/eslint-plugin-metamask-design-tokens) library to the MetaMask extension codebase, along with the implementation of the [color-no-hex](https://github.com/MetaMask/eslint-plugin-design-tokens/blob/main/docs/rules/color-no-hex.md) rule. This rule aims to prevent the use of static hex color values in our `.js`, `.ts` and `.tsx` files encouraging instead the adoption of design tokens. The motivation behind this change is to ensure our styling remains consistent with the MetaMask design system, enhancing themability, accessibility, and overall alignment with our design principles. Additionally, this update is a crucial step towards enabling the seamless rollout of future brand evolution within MetaMask, ensuring that our UI can adapt to new design standards with minimal effort. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/24111 ## **Manual testing steps** To ensure the successful integration of the `eslint-plugin-design-tokens` and the effectiveness of the `color-no-hex` rule: 1. Install the updated dependencies by running `yarn install`. 2. Execute `yarn lint:check` to identify any instances of static hex color values. 3. Review the linting output and update any flagged instances to use the corresponding design tokens. 4. Navigate through the extension's UI to verify that the updates maintain visual consistency and adhere to the design system. ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-extension/assets/8112138/b883f26c-f0e1-4356-b828-58531a76799a ### **After** https://github.com/MetaMask/metamask-extension/assets/8112138/bb9b0d53-ef8b-44bf-a913-b6ae73b6df4d ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability. - [x] I’ve included tests if applicable. - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable. - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g., pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 17 +++++++++++++++++ app/scripts/controllers/preferences.js | 3 ++- package.json | 1 + test/e2e/tests/metrics/mock-data.js | 1 + .../ui/metafox-logo/horizontal-logo.js | 1 + ui/helpers/constants/settings.js | 1 + .../status-slider/status-slider.js | 2 ++ .../pin-mmi-billboard/pin-mmi-billboard.js | 1 + .../pin-extension/pin-billboard.js | 1 + .../networks-tab/networks-tab.constants.js | 12 ++++++++---- .../background-animation.js | 1 + .../loading-swaps-quotes-stories-metadata.js | 1 + .../swaps/main-quote-summary/quote-backdrop.js | 1 + .../mascot-background-animation.js | 1 + ui/pages/swaps/swaps-util-test-constants.js | 5 +++-- yarn.lock | 8 ++++++++ 16 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 53235d12c55a..a2207e6ba560 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,10 @@ module.exports = { ignorePatterns: readFileSync('.prettierignore', 'utf8').trim().split('\n'), // eslint's parser, esprima, is not compatible with ESM, so use the babel parser instead parser: '@babel/eslint-parser', + plugins: ['@metamask/design-tokens'], + rules: { + '@metamask/design-tokens/color-no-hex': 'warn', + }, overrides: [ /** * == Modules == @@ -439,5 +443,18 @@ module.exports = { ], }, }, + /** + * Don't check for static hex values in .test, .spec or .stories files + */ + { + files: [ + '**/*.test.{js,ts,tsx}', + '**/*.spec.{js,ts,tsx}', + '**/*.stories.{js,ts,tsx}', + ], + rules: { + '@metamask/design-tokens/color-no-hex': 'off', + }, + }, ], }; diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index ec852e882c6a..4c9fcc7ffb57 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -442,7 +442,8 @@ export default class PreferencesController { */ syncAddresses(addresses) { if (!Array.isArray(addresses) || addresses.length === 0) { - throw new Error('Expected non-empty array of addresses. Error #11201'); + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + throw new Error('Expected non-empty array of addresses. Error #11201'); // not a hex color value } const { identities, lostIdentities } = this.store.getState(); diff --git a/package.json b/package.json index 5d435086dbb2..1b47d0f7b954 100644 --- a/package.json +++ b/package.json @@ -436,6 +436,7 @@ "@metamask/eslint-config-mocha": "^9.0.0", "@metamask/eslint-config-nodejs": "^9.0.0", "@metamask/eslint-config-typescript": "^9.0.1", + "@metamask/eslint-plugin-design-tokens": "^1.1.0", "@metamask/forwarder": "^1.1.0", "@metamask/phishing-warning": "^3.0.3", "@metamask/test-bundler": "^1.0.0", diff --git a/test/e2e/tests/metrics/mock-data.js b/test/e2e/tests/metrics/mock-data.js index fe68d8e7d29c..d37c4f4bc686 100644 --- a/test/e2e/tests/metrics/mock-data.js +++ b/test/e2e/tests/metrics/mock-data.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ const TOKENS_API_MOCK_RESULT = [ { name: 'Ethereum', diff --git a/ui/components/ui/metafox-logo/horizontal-logo.js b/ui/components/ui/metafox-logo/horizontal-logo.js index ca80968bd42c..e2bea0127c29 100644 --- a/ui/components/ui/metafox-logo/horizontal-logo.js +++ b/ui/components/ui/metafox-logo/horizontal-logo.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { ThemeType } from '../../../../shared/constants/preferences'; diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 33cb39c0ac5c..431b438e3c36 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import { IconName } from '../../components/component-library'; import { ALERTS_ROUTE, diff --git a/ui/pages/confirmations/components/edit-gas-fee-popover/network-statistics/status-slider/status-slider.js b/ui/pages/confirmations/components/edit-gas-fee-popover/network-statistics/status-slider/status-slider.js index a592670ed07c..29b1f1c62080 100644 --- a/ui/pages/confirmations/components/edit-gas-fee-popover/network-statistics/status-slider/status-slider.js +++ b/ui/pages/confirmations/components/edit-gas-fee-popover/network-statistics/status-slider/status-slider.js @@ -5,6 +5,7 @@ import { useGasFeeContext } from '../../../../../../contexts/gasFee'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; import { NetworkStabilityTooltip } from '../tooltips'; +/* eslint-disable @metamask/design-tokens/color-no-hex */ const GRADIENT_COLORS = [ '#037DD6', '#1876C8', @@ -18,6 +19,7 @@ const GRADIENT_COLORS = [ '#C54055', '#D73A49', ]; +/* eslint-enable @metamask/design-tokens/color-no-hex */ const determineStatusInfo = (givenNetworkCongestion) => { const networkCongestion = givenNetworkCongestion ?? 0.5; diff --git a/ui/pages/institutional/pin-mmi-billboard/pin-mmi-billboard.js b/ui/pages/institutional/pin-mmi-billboard/pin-mmi-billboard.js index 008c5bf516b7..57964793bd23 100644 --- a/ui/pages/institutional/pin-mmi-billboard/pin-mmi-billboard.js +++ b/ui/pages/institutional/pin-mmi-billboard/pin-mmi-billboard.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import React from 'react'; import { useI18nContext } from '../../../hooks/useI18nContext'; diff --git a/ui/pages/onboarding-flow/pin-extension/pin-billboard.js b/ui/pages/onboarding-flow/pin-extension/pin-billboard.js index 0cc28daf06b1..d4a707cb1f12 100644 --- a/ui/pages/onboarding-flow/pin-extension/pin-billboard.js +++ b/ui/pages/onboarding-flow/pin-extension/pin-billboard.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import React from 'react'; import { useI18nContext } from '../../../hooks/useI18nContext'; diff --git a/ui/pages/settings/networks-tab/networks-tab.constants.js b/ui/pages/settings/networks-tab/networks-tab.constants.js index a4ab54b88a71..8e61213bbb8e 100644 --- a/ui/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/pages/settings/networks-tab/networks-tab.constants.js @@ -9,7 +9,8 @@ import { const defaultNetworksData = [ { labelKey: NETWORK_TYPES.MAINNET, - iconColor: '#29B6AF', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + iconColor: '#29B6AF', // third party color providerType: NETWORK_TYPES.MAINNET, rpcUrl: getRpcUrl({ network: NETWORK_TYPES.MAINNET, @@ -21,7 +22,8 @@ const defaultNetworksData = [ }, { labelKey: NETWORK_TYPES.SEPOLIA, - iconColor: '#CFB5F0', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + iconColor: '#CFB5F0', // third party color providerType: NETWORK_TYPES.SEPOLIA, rpcUrl: getRpcUrl({ network: NETWORK_TYPES.SEPOLIA, @@ -33,7 +35,8 @@ const defaultNetworksData = [ }, { labelKey: NETWORK_TYPES.LINEA_SEPOLIA, - iconColor: '#61dfff', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + iconColor: '#61dfff', // third party color providerType: NETWORK_TYPES.LINEA_SEPOLIA, rpcUrl: getRpcUrl({ network: NETWORK_TYPES.LINEA_SEPOLIA, @@ -45,7 +48,8 @@ const defaultNetworksData = [ }, { labelKey: NETWORK_TYPES.LINEA_MAINNET, - iconColor: '#121212', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + iconColor: '#121212', // third party color providerType: NETWORK_TYPES.LINEA_MAINNET, rpcUrl: getRpcUrl({ network: NETWORK_TYPES.LINEA_MAINNET, diff --git a/ui/pages/swaps/loading-swaps-quotes/background-animation.js b/ui/pages/swaps/loading-swaps-quotes/background-animation.js index 6cbbfea1cbb1..c3a144c30aec 100644 --- a/ui/pages/swaps/loading-swaps-quotes/background-animation.js +++ b/ui/pages/swaps/loading-swaps-quotes/background-animation.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import React from 'react'; export default function BackgroundAnimation() { diff --git a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.js b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.js index 0915fe3b5df8..36e0b5ba089d 100644 --- a/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.js +++ b/ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes-stories-metadata.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ export const storiesMetadata = { totle: { color: '#283B4C', diff --git a/ui/pages/swaps/main-quote-summary/quote-backdrop.js b/ui/pages/swaps/main-quote-summary/quote-backdrop.js index 77bd75fd68e0..44351c73bb5d 100644 --- a/ui/pages/swaps/main-quote-summary/quote-backdrop.js +++ b/ui/pages/swaps/main-quote-summary/quote-backdrop.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js b/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js index 805c56f7a3a5..a8fddf9943d7 100644 --- a/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js +++ b/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js @@ -1,3 +1,4 @@ +/* eslint-disable @metamask/design-tokens/color-no-hex*/ import EventEmitter from 'events'; import React, { useRef } from 'react'; diff --git a/ui/pages/swaps/swaps-util-test-constants.js b/ui/pages/swaps/swaps-util-test-constants.js index aa02f6a92487..4c69fd1077cd 100644 --- a/ui/pages/swaps/swaps-util-test-constants.js +++ b/ui/pages/swaps/swaps-util-test-constants.js @@ -1,3 +1,4 @@ +import { lightTheme } from '@metamask/design-tokens'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; export const TRADES_BASE_PROD_URL = @@ -161,12 +162,12 @@ export const MOCK_TRADE_RESPONSE_2 = MOCK_TRADE_RESPONSE_1.map((trade) => ({ export const AGGREGATOR_METADATA = { agg1: { - color: '#283B4C', + color: lightTheme.colors.text.default, title: 'agg1', icon: 'data:image/png;base64,iVBORw0KGgoAAA', }, agg2: { - color: '#283B4C', + color: lightTheme.colors.text.default, title: 'agg2', icon: 'data:image/png;base64,iVBORw0KGgoAAA', }, diff --git a/yarn.lock b/yarn.lock index 6ea951bcc6d9..62c06c0bf54c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4392,6 +4392,13 @@ __metadata: languageName: node linkType: hard +"@metamask/eslint-plugin-design-tokens@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/eslint-plugin-design-tokens@npm:1.1.0" + checksum: 21ee903ed25eb1edb3b6f4fc4e426dc48bde0ad5d338618b5b820a11dc5f33a2706527063dca8b50e9049d3c6743781c5ae33d5860088deabc1f250659815a94 + languageName: node + linkType: hard + "@metamask/eth-hd-keyring@npm:^7.0.1": version: 7.0.1 resolution: "@metamask/eth-hd-keyring@npm:7.0.1" @@ -24882,6 +24889,7 @@ __metadata: "@metamask/eslint-config-mocha": "npm:^9.0.0" "@metamask/eslint-config-nodejs": "npm:^9.0.0" "@metamask/eslint-config-typescript": "npm:^9.0.1" + "@metamask/eslint-plugin-design-tokens": "npm:^1.1.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" "@metamask/eth-json-rpc-middleware": "npm:^12.1.0" "@metamask/eth-keyring-controller": "npm:^16.0.0" From 8860a107376884b574d009d5cd4ba3ebb000bc19 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 25 Apr 2024 10:43:52 -0500 Subject: [PATCH 003/107] fix: Provider padding to address button in Receive modal (#24200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The "Receive tokens" modal doesn't have padding around the AddressCopyButton and it looks bad. This PR adds padding in. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24200?quickstart=1) ## **Related issues** Fixes: N/A ## **Manual testing steps** 1. On the wallet screen, click "Receive tokens" 2. See padding around the AddressCopyButton ## **Screenshots/Recordings** ### **Before** SCR-20240423-kyfy ### **After** SCR-20240423-kybe ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/components/multichain/receive-modal/receive-modal.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/multichain/receive-modal/receive-modal.js b/ui/components/multichain/receive-modal/receive-modal.js index 3d5bb2746856..8fe57cc9de5a 100644 --- a/ui/components/multichain/receive-modal/receive-modal.js +++ b/ui/components/multichain/receive-modal/receive-modal.js @@ -61,11 +61,12 @@ export const ReceiveModal = ({ address, onClose }) => { > {name} - From f6728c88629c21b6f21fc23e9330256d3717155f Mon Sep 17 00:00:00 2001 From: Matteo Scurati Date: Fri, 26 Apr 2024 12:35:04 +0200 Subject: [PATCH 004/107] feat: push notifications new controller (#23137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a new SW (Service Worker) and a new controller for managing push notifications. This integration, due to the differences between MV2 and MV3 regarding the use of background scripts and the SW, is only related to the manifest V2. For the manifest V3, part of this logic will be modified, using the extension's own service worker as a starting point. With manifest V2, on the contrary, it is necessary to initialize a dedicated service worker so that FCM (Firebase Cloud Messaging) can handle messages received in the background. The final UI will allow the user to enable/disable the receipt of notifications. However, this PR concerns only the new controller and the build management for the creation of the SW. The new controller will work in accordance with two other controllers for managing authentication and for managing the triggers necessary to receive notifications. ## **Related issues** No related issues. Fixes: ## **Manual testing steps** At the moment, the necessary environment variables have not been inserted into the build process. To test the branch locally, feel free to ask me for the env to insert into your local .metamaskrc file ## **Screenshots/Recordings** In this video, I show the interaction between a test server for sending notifications and the extension: https://www.loom.com/share/702fc6a8aea74510acdb1bfac614fa4a ### **Before** ### **After** Check the entire flow here https://www.figma.com/file/c7GgNw2kScGrVyRGAPhwEd/Notifications?type=design&node-id=484%3A44713&mode=design&t=QsKrmdqZnx42IrMt-1 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've clearly explained what problem this PR is solving and how it is solved. - [ ] I've linked related issues - [ ] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [ ] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [x] I’ve properly set the pull request status: - [x] In case it's not yet "ready for review", I've set it to "draft". - [ ] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Prithpal Sooriya --- app/_locales/en/messages.json | 72 ++ .../constants/notification-schema.ts | 10 + .../push-platform-notifications.test.ts | 152 +++++ .../push-platform-notifications.ts | 182 +++++ .../services/services.test.ts | 209 ++++++ .../services/services.ts | 278 ++++++++ .../types/firebase.ts | 46 ++ .../utils/get-notification-data.test.ts | 80 +++ .../utils/get-notification-data.ts | 90 +++ .../utils/get-notification-message.test.ts | 150 ++++ .../utils/get-notification-message.ts | 246 +++++++ app/scripts/metamask-controller.js | 10 + builds.yml | 10 + development/build/scripts.js | 8 + development/verify-locale-strings.js | 1 + jest.config.js | 1 + lavamoat/browserify/beta/policy.json | 115 ++++ lavamoat/browserify/desktop/policy.json | 115 ++++ lavamoat/browserify/flask/policy.json | 115 ++++ lavamoat/browserify/main/policy.json | 115 ++++ lavamoat/browserify/mmi/policy.json | 115 ++++ lavamoat/build-system/policy.json | 54 +- package.json | 4 +- yarn.lock | 642 +++++++++++++++++- 24 files changed, 2787 insertions(+), 33 deletions(-) create mode 100644 app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts create mode 100644 app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts create mode 100644 app/scripts/controllers/push-platform-notifications/services/services.test.ts create mode 100644 app/scripts/controllers/push-platform-notifications/services/services.ts create mode 100644 app/scripts/controllers/push-platform-notifications/types/firebase.ts create mode 100644 app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts create mode 100644 app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts create mode 100644 app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts create mode 100644 app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f1e01aa12ad8..7cbc09d4087e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3754,6 +3754,78 @@ "publicAddress": { "message": "Public address" }, + "pushPlatformNotificationsFundsReceivedDescription": { + "message": "You received $1 $2" + }, + "pushPlatformNotificationsFundsReceivedDescriptionDefault": { + "message": "You received some tokens" + }, + "pushPlatformNotificationsFundsReceivedTitle": { + "message": "Funds received" + }, + "pushPlatformNotificationsFundsSentDescription": { + "message": "You successfully sent $1 $2" + }, + "pushPlatformNotificationsFundsSentDescriptionDefault": { + "message": "You successfully sent some tokens" + }, + "pushPlatformNotificationsFundsSentTitle": { + "message": "Funds sent" + }, + "pushPlatformNotificationsNftReceivedDescription": { + "message": "You received new NFTs" + }, + "pushPlatformNotificationsNftReceivedTitle": { + "message": "NFT received" + }, + "pushPlatformNotificationsNftSentDescription": { + "message": "You have successfully sent an NFT" + }, + "pushPlatformNotificationsNftSentTitle": { + "message": "NFT sent" + }, + "pushPlatformNotificationsStakingLidoStakeCompletedDescription": { + "message": "Your Lido stake was successful" + }, + "pushPlatformNotificationsStakingLidoStakeCompletedTitle": { + "message": "Stake complete" + }, + "pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription": { + "message": "Your Lido stake is now ready to be withdrawn" + }, + "pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle": { + "message": "Stake ready for withdrawal" + }, + "pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription": { + "message": "Your Lido withdrawal was successful" + }, + "pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle": { + "message": "Withdrawal completed" + }, + "pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription": { + "message": "Your Lido withdrawal request was submitted" + }, + "pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle": { + "message": "Withdrawal requested" + }, + "pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription": { + "message": "Your RocketPool stake was successful" + }, + "pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle": { + "message": "Stake complete" + }, + "pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription": { + "message": "Your RocketPool unstake was successful" + }, + "pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle": { + "message": "Unstake complete" + }, + "pushPlatformNotificationsSwapCompletedDescription": { + "message": "Your MetaMask Swap was successful" + }, + "pushPlatformNotificationsSwapCompletedTitle": { + "message": "Swap completed" + }, "queued": { "message": "Queued" }, diff --git a/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts b/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts index c1661cb7fcf6..77b791f3a5a8 100644 --- a/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts +++ b/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts @@ -33,6 +33,16 @@ export const NOTIFICATION_CHAINS = { LINEA: '59144', }; +export const CHAIN_SYMBOLS = { + [NOTIFICATION_CHAINS.ETHEREUM]: 'ETH', + [NOTIFICATION_CHAINS.OPTIMISM]: 'ETH', + [NOTIFICATION_CHAINS.BSC]: 'BNB', + [NOTIFICATION_CHAINS.POLYGON]: 'MATIC', + [NOTIFICATION_CHAINS.ARBITRUM]: 'ETH', + [NOTIFICATION_CHAINS.AVALANCHE]: 'AVAX', + [NOTIFICATION_CHAINS.LINEA]: 'ETH', +}; + export const SUPPORTED_CHAINS = [ NOTIFICATION_CHAINS.ETHEREUM, NOTIFICATION_CHAINS.OPTIMISM, diff --git a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts new file mode 100644 index 000000000000..a237fd0a4684 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts @@ -0,0 +1,152 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import type { AuthenticationControllerGetBearerToken } from '../authentication/authentication-controller'; +import { PushPlatformNotificationsController } from './push-platform-notifications'; + +import * as services from './services/services'; +import type { + PushPlatformNotificationsControllerMessenger, + PushPlatformNotificationsControllerState, +} from './push-platform-notifications'; + +const MOCK_JWT = 'mockJwt'; +const MOCK_FCM_TOKEN = 'mockFcmToken'; +const MOCK_TRIGGERS = ['uuid1', 'uuid2']; + +describe('PushPlatformNotificationsController', () => { + describe('enablePushNotifications', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should update the state with the fcmToken', async () => { + await withController(async ({ controller, messenger }) => { + mockAuthBearerTokenCall(messenger); + jest + .spyOn(services, 'activatePushNotifications') + .mockResolvedValue(MOCK_FCM_TOKEN); + + await controller.enablePushNotifications(MOCK_TRIGGERS); + expect(controller.state.fcmToken).toBe(MOCK_FCM_TOKEN); + }); + }); + + it('should fail if a jwt token is not provided', async () => { + await withController(async ({ messenger, controller }) => { + mockAuthBearerTokenCall(messenger).mockResolvedValue( + null as unknown as string, + ); + await expect(controller.enablePushNotifications([])).rejects.toThrow(); + }); + }); + }); + + describe('disablePushNotifications', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should update the state removing the fcmToken', async () => { + await withController(async ({ messenger, controller }) => { + mockAuthBearerTokenCall(messenger); + await controller.disablePushNotifications(MOCK_TRIGGERS); + expect(controller.state.fcmToken).toBe(''); + }); + }); + + it('should fail if a jwt token is not provided', async () => { + await withController(async ({ messenger, controller }) => { + mockAuthBearerTokenCall(messenger).mockResolvedValue( + null as unknown as string, + ); + await expect(controller.disablePushNotifications([])).rejects.toThrow(); + }); + }); + }); + + describe('updateTriggerPushNotifications', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call updateTriggerPushNotifications with the correct parameters', async () => { + await withController(async ({ messenger, controller }) => { + mockAuthBearerTokenCall(messenger); + const spy = jest + .spyOn(services, 'updateTriggerPushNotifications') + .mockResolvedValue(true); + + await controller.updateTriggerPushNotifications(MOCK_TRIGGERS); + + expect(spy).toHaveBeenCalledWith( + controller.state.fcmToken, + MOCK_JWT, + MOCK_TRIGGERS, + ); + }); + }); + }); +}); + +// Test helper functions + +type WithControllerCallback = ({ + controller, + initialState, + messenger, +}: { + controller: PushPlatformNotificationsController; + initialState: PushPlatformNotificationsControllerState; + messenger: PushPlatformNotificationsControllerMessenger; +}) => Promise | ReturnValue; + +function buildMessenger() { + return new ControllerMessenger< + AuthenticationControllerGetBearerToken, + never + >(); +} + +function buildPushPlatformNotificationsControllerMessenger( + messenger = buildMessenger(), +) { + return messenger.getRestricted({ + name: 'PushPlatformNotificationsController', + allowedActions: ['AuthenticationController:getBearerToken'], + }) as PushPlatformNotificationsControllerMessenger; +} + +async function withController( + fn: WithControllerCallback, +): Promise { + const messenger = buildPushPlatformNotificationsControllerMessenger(); + const controller = new PushPlatformNotificationsController({ + messenger, + state: { fcmToken: '' }, + }); + + return await fn({ + controller, + initialState: controller.state, + messenger, + }); +} + +function mockAuthBearerTokenCall( + messenger: PushPlatformNotificationsControllerMessenger, +) { + type Fn = AuthenticationControllerGetBearerToken['handler']; + const mockAuthGetBearerToken = jest + .fn, Parameters>() + .mockResolvedValue(MOCK_JWT); + + jest.spyOn(messenger, 'call').mockImplementation((...args) => { + const [actionType] = args; + if (actionType === 'AuthenticationController:getBearerToken') { + return mockAuthGetBearerToken(); + } + + throw new Error('MOCK - unsupported messenger call mock'); + }); + + return mockAuthGetBearerToken; +} diff --git a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts new file mode 100644 index 000000000000..80c799074853 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts @@ -0,0 +1,182 @@ +import { + BaseController, + RestrictedControllerMessenger, + ControllerGetStateAction, +} from '@metamask/base-controller'; +import log from 'loglevel'; + +import type { AuthenticationControllerGetBearerToken } from '../authentication/authentication-controller'; +import { + activatePushNotifications, + deactivatePushNotifications, + updateTriggerPushNotifications, +} from './services/services'; + +const controllerName = 'PushPlatformNotificationsController'; + +export type PushPlatformNotificationsControllerState = { + fcmToken: string; +}; + +export declare type PushPlatformNotificationsControllerEnablePushNotificationsAction = + { + type: `${typeof controllerName}:enablePushNotifications`; + handler: PushPlatformNotificationsController['enablePushNotifications']; + }; + +export declare type PushPlatformNotificationsControllerDisablePushNotificationsAction = + { + type: `${typeof controllerName}:disablePushNotifications`; + handler: PushPlatformNotificationsController['disablePushNotifications']; + }; + +export type PushPlatformNotificationsControllerMessengerActions = + | PushPlatformNotificationsControllerEnablePushNotificationsAction + | PushPlatformNotificationsControllerDisablePushNotificationsAction + | ControllerGetStateAction<'state', PushPlatformNotificationsControllerState>; + +type AllowedActions = AuthenticationControllerGetBearerToken; + +export type PushPlatformNotificationsControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + PushPlatformNotificationsControllerMessengerActions | AllowedActions, + never, + AllowedActions['type'], + never + >; + +const metadata = { + fcmToken: { + persist: true, + anonymous: true, + }, +}; + +/** + * Manages push notifications for the application, including enabling, disabling, and updating triggers for push notifications. + * This controller integrates with Firebase Cloud Messaging (FCM) to handle the registration and management of push notifications. + * It is responsible for registering and unregistering the service worker that listens for push notifications, + * managing the FCM token, and communicating with the server to register or unregister the device for push notifications. + * Additionally, it provides functionality to update the server with new UUIDs that should trigger push notifications. + * + * @augments {BaseController} + */ +export class PushPlatformNotificationsController extends BaseController< + typeof controllerName, + PushPlatformNotificationsControllerState, + PushPlatformNotificationsControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: PushPlatformNotificationsControllerMessenger; + state: PushPlatformNotificationsControllerState; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { + fcmToken: state?.fcmToken || '', + }, + }); + } + + async #getAndAssertBearerToken() { + const bearerToken = await this.messagingSystem.call( + 'AuthenticationController:getBearerToken', + ); + if (!bearerToken) { + log.error( + 'Failed to enable push notifications: BearerToken token is missing.', + ); + throw new Error('BearerToken token is missing'); + } + + return bearerToken; + } + + /** + * Enables push notifications for the application. + * + * This method sets up the necessary infrastructure for handling push notifications by: + * 1. Registering the service worker to listen for messages. + * 2. Fetching the Firebase Cloud Messaging (FCM) token from Firebase. + * 3. Sending the FCM token to the server responsible for sending notifications, to register the device. + * + * @param UUIDs - An array of UUIDs to enable push notifications for. + */ + public async enablePushNotifications(UUIDs: string[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + + try { + // 2. Call the activatePushNotifications method from PushPlatformNotificationsUtils + const regToken = await activatePushNotifications(bearerToken, UUIDs); + + // 3. Update the state with the FCM token + if (regToken) { + this.update((state) => { + state.fcmToken = regToken; + }); + } + } catch (error) { + log.error('Failed to enable push notifications:', error); + throw new Error('Failed to enable push notifications'); + } + } + + /** + * Disables push notifications for the application. + * This method handles the process of disabling push notifications by: + * 1. Unregistering the service worker to stop listening for messages. + * 2. Sending a request to the server to unregister the device using the FCM token. + * 3. Removing the FCM token from the state to complete the process. + * + * @param UUIDs - An array of UUIDs for which push notifications should be disabled. + */ + public async disablePushNotifications(UUIDs: string[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + let isPushNotificationsDisabled: boolean; + + try { + // 1. Send a request to the server to unregister the token/device + isPushNotificationsDisabled = await deactivatePushNotifications( + this.state.fcmToken, + bearerToken, + UUIDs, + ); + } catch (error) { + const errorMessage = `Failed to disable push notifications: ${error}`; + log.error(errorMessage); + throw new Error(errorMessage); + } + + // 2. Remove the FCM token from the state + if (isPushNotificationsDisabled) { + this.update((state) => { + state.fcmToken = ''; + }); + } + } + + /** + * Updates the triggers for push notifications. + * This method is responsible for updating the server with the new set of UUIDs that should trigger push notifications. + * It uses the current FCM token and a BearerToken for authentication. + * + * @param UUIDs - An array of UUIDs that should trigger push notifications. + */ + public async updateTriggerPushNotifications(UUIDs: string[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + + try { + updateTriggerPushNotifications(this.state.fcmToken, bearerToken, UUIDs); + } catch (error) { + const errorMessage = `Failed to update triggers for push notifications: ${error}`; + log.error(errorMessage); + throw new Error(errorMessage); + } + } +} diff --git a/app/scripts/controllers/push-platform-notifications/services/services.test.ts b/app/scripts/controllers/push-platform-notifications/services/services.test.ts new file mode 100644 index 000000000000..c15473849dfe --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/services/services.test.ts @@ -0,0 +1,209 @@ +import * as services from './services'; + +type MockResponse = { + trigger_ids: string[]; + registration_tokens: string[]; +}; + +const MOCK_REG_TOKEN = 'REG_TOKEN'; +const MOCK_NEW_REG_TOKEN = 'NEW_REG_TOKEN'; +const MOCK_TRIGGERS = ['1', '2', '3']; +const MOCK_RESPONSE: MockResponse = { + trigger_ids: ['1', '2', '3'], + registration_tokens: ['reg-token-1', 'reg-token-2'], +}; +const MOCK_JWT = 'MOCK_JWT'; + +describe('PushPlatformNotificationsServices', () => { + describe('getPushNotificationLinks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const utils = services; + + it('Should return reg token links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValue(MOCK_RESPONSE); + + const res = await services.getPushNotificationLinks(MOCK_JWT); + + expect(res).toBeDefined(); + expect(res?.trigger_ids).toBeDefined(); + expect(res?.registration_tokens).toBeDefined(); + }); + + it('Should return null if api call fails', async () => { + jest.spyOn(services, 'getPushNotificationLinks').mockResolvedValue(null); + + const res = await utils.getPushNotificationLinks(MOCK_JWT); + expect(res).toBeNull(); + }); + }); + + describe('updateLinksAPI', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should return true if links are updated', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(true); + + const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ + MOCK_NEW_REG_TOKEN, + ]); + + expect(res).toBe(true); + }); + + it('Should return false if links are not updated', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + + const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ + MOCK_NEW_REG_TOKEN, + ]); + + expect(res).toBe(false); + }); + }); + + describe('activatePushNotifications()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should append registration token when enabling push', async () => { + jest + .spyOn(services, 'activatePushNotifications') + .mockResolvedValue(MOCK_NEW_REG_TOKEN); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(MOCK_NEW_REG_TOKEN); + }); + + it('should fail if unable to get existing notification links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(null); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + it('should fail if unable to create new reg token', async () => { + jest.spyOn(services, 'createRegToken').mockResolvedValueOnce(null); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + it('should fail if unable to update links', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValueOnce(false); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); + + describe('deactivatePushNotifications()', () => { + it('should fail if unable to get existing notification links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(null); + + const res = await services.deactivatePushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + it('should fail if unable to update links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValue(MOCK_RESPONSE); + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + + const res = await services.deactivatePushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + it('should fail if unable to delete reg token', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(MOCK_RESPONSE); + jest.spyOn(services, 'deleteRegToken').mockResolvedValue(false); + + const res = await services.deactivatePushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + }); + + describe('updateTriggerPushNotifications()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should update triggers for push notifications', async () => { + jest + .spyOn(services, 'updateTriggerPushNotifications') + .mockResolvedValue(true); + + const res = await services.updateTriggerPushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(true); + }); + + it('should fail if unable to update triggers', async () => { + jest + .spyOn(services, 'updateTriggerPushNotifications') + .mockResolvedValue(false); + + const res = await services.updateTriggerPushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); +}); diff --git a/app/scripts/controllers/push-platform-notifications/services/services.ts b/app/scripts/controllers/push-platform-notifications/services/services.ts new file mode 100644 index 000000000000..c73f11f6cc66 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/services/services.ts @@ -0,0 +1,278 @@ +import { getToken, deleteToken } from 'firebase/messaging'; +import type { FirebaseApp } from 'firebase/app'; +import { getApp, initializeApp } from 'firebase/app'; +import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw'; +import type { Messaging, MessagePayload } from 'firebase/messaging/sw'; +import log from 'loglevel'; +import { onPushNotification } from '../utils/get-notification-message'; + +const url = process.env.PUSH_NOTIFICATIONS_SERVICE_URL; +const REGISTRATION_TOKENS_ENDPOINT = `${url}/v1/link`; +const sw = self as unknown as ServiceWorkerGlobalScope; + +export type LinksResult = { + trigger_ids: string[]; + registration_tokens: string[]; +}; + +/** + * Attempts to retrieve an existing Firebase app instance. If no instance exists, it initializes a new app with the provided configuration. + * + * @returns The Firebase app instance. + */ +export async function createFirebaseApp(): Promise { + try { + return getApp(); + } catch { + const firebaseConfig = { + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + storageBucket: process.env.FIREBASE_STORAGE_BUCKET, + projectId: process.env.FIREBASE_PROJECT_ID, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID, + measurementId: process.env.FIREBASE_MEASUREMENT_ID, + }; + return initializeApp(firebaseConfig); + } +} + +/** + * Retrieves the Firebase Messaging service instance. + * + * This function first ensures a Firebase app instance is created or retrieved by calling `createFirebaseApp`. + * It then initializes and returns the Firebase Messaging service associated with the Firebase app. + * + * @returns A promise that resolves with the Firebase Messaging service instance. + */ +export async function getFirebaseMessaging(): Promise { + const app = await createFirebaseApp(); + return getMessaging(app); +} + +/** + * Creates a registration token for Firebase Cloud Messaging. + * + * @returns A promise that resolves with the registration token or null if an error occurs. + */ +export async function createRegToken(): Promise { + try { + const messaging = await getFirebaseMessaging(); + const token = await getToken(messaging, { + serviceWorkerRegistration: sw.registration, + vapidKey: process.env.VAPID_KEY, + }); + return token; + } catch { + return null; + } +} + +/** + * Deletes the Firebase Cloud Messaging registration token. + * + * @returns A promise that resolves with true if the token was successfully deleted, false otherwise. + */ +export async function deleteRegToken(): Promise { + try { + const messaging = await getFirebaseMessaging(); + await deleteToken(messaging); + return true; + } catch (error) { + return false; + } +} + +/** + * Fetches push notification links from a remote endpoint using a BearerToken for authorization. + * + * @param bearerToken - The JSON Web Token used for authorization. + * @returns A promise that resolves with the links result or null if an error occurs. + */ +export async function getPushNotificationLinks( + bearerToken: string, +): Promise { + try { + const response = await fetch(REGISTRATION_TOKENS_ENDPOINT, { + headers: { Authorization: `Bearer ${bearerToken}` }, + }); + if (!response.ok) { + throw new Error('Failed to fetch links'); + } + return response.json() as Promise; + } catch { + return null; + } +} + +/** + * Updates the push notification links on a remote API. + * + * @param bearerToken - The JSON Web Token used for authorization. + * @param triggers - An array of trigger identifiers. + * @param regTokens - An array of registration tokens. + * @returns A promise that resolves with true if the update was successful, false otherwise. + */ +export async function updateLinksAPI( + bearerToken: string, + triggers: string[], + regTokens: string[], +): Promise { + try { + const body: LinksResult = { + trigger_ids: triggers, + registration_tokens: regTokens, + }; + const response = await fetch( + `${process.env.PUSH_NOTIFICATIONS_SERVICE_URL}/v1/link`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${bearerToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }, + ); + return response.status === 200; + } catch { + return false; + } +} + +/** + * Enables push notifications by registering the device and linking triggers. + * + * @param bearerToken - The JSON Web Token used for authorization. + * @param triggers - An array of trigger identifiers. + * @returns A promise that resolves with an object containing the success status and the BearerToken token. + */ +export async function activatePushNotifications( + bearerToken: string, + triggers: string[], +): Promise { + const notificationLinks = await getPushNotificationLinks(bearerToken); + if (!notificationLinks) { + return null; + } + + const regToken = await createRegToken().catch(() => null); + if (!regToken) { + return null; + } + + const messaging = await getFirebaseMessaging(); + + onBackgroundMessage( + messaging, + async (payload: MessagePayload): Promise => { + const typedPayload = payload; + + // if the payload does not contain data, do nothing + try { + const notificationData = typedPayload?.data?.data + ? JSON.parse(typedPayload?.data?.data) + : undefined; + if (!notificationData) { + return; + } + + await onPushNotification(notificationData); + } catch (error) { + // Do Nothing, cannot parse a bad notification + log.error('Unable to send push notification:', { + notification: payload?.data?.data, + error, + }); + throw new Error('Unable to send push notification'); + } + }, + ); + + const newRegTokens = new Set(notificationLinks.registration_tokens); + newRegTokens.add(regToken); + + await updateLinksAPI(bearerToken, triggers, Array.from(newRegTokens)); + return regToken; +} + +/** + * Disables push notifications by removing the registration token and unlinking triggers. + * + * @param regToken - The registration token to be removed. + * @param bearerToken - The JSON Web Token used for authorization. + * @param triggers - An array of trigger identifiers to be unlinked. + * @returns A promise that resolves with true if notifications were successfully disabled, false otherwise. + */ +export async function deactivatePushNotifications( + regToken: string, + bearerToken: string, + triggers: string[], +): Promise { + // if we don't have a reg token, then we can early return + if (!regToken) { + return true; + } + + const notificationLinks = await getPushNotificationLinks(bearerToken); + if (!notificationLinks) { + return false; + } + + const regTokenSet = new Set(notificationLinks.registration_tokens); + regTokenSet.delete(regToken); + + const isTokenRemovedFromAPI = await updateLinksAPI( + bearerToken, + triggers, + Array.from(regTokenSet), + ); + if (!isTokenRemovedFromAPI) { + return false; + } + + const isTokenRemovedFromFCM = await deleteRegToken(); + if (!isTokenRemovedFromFCM) { + return false; + } + + return true; +} + +/** + * Updates the triggers linked to push notifications for a given registration token. + * + * @param regToken - The registration token to update triggers for. + * @param bearerToken - The JSON Web Token used for authorization. + * @param triggers - An array of new trigger identifiers to link. + * @returns A promise that resolves with true if the triggers were successfully updated, false otherwise. + */ +export async function updateTriggerPushNotifications( + regToken: string, + bearerToken: string, + triggers: string[], +): Promise { + const notificationLinks = await getPushNotificationLinks(bearerToken); + if (!notificationLinks) { + return false; + } + + // Create new registration token if doesn't exist + const regTokenSet = new Set(notificationLinks.registration_tokens); + if (!regToken || !regTokenSet.has(regToken)) { + await deleteRegToken(); + const newRegToken = await createRegToken(); + if (!newRegToken) { + throw new Error('Failed to create a new registration token'); + } + regTokenSet.add(newRegToken); + } + + const isTriggersLinkedToPushNotifications = await updateLinksAPI( + bearerToken, + triggers, + Array.from(regTokenSet), + ); + + return isTriggersLinkedToPushNotifications; +} diff --git a/app/scripts/controllers/push-platform-notifications/types/firebase.ts b/app/scripts/controllers/push-platform-notifications/types/firebase.ts new file mode 100644 index 000000000000..91dc2c2e4d6f --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/types/firebase.ts @@ -0,0 +1,46 @@ +export declare type Messaging = { + app: FirebaseApp; +}; + +export declare type FirebaseApp = { + readonly name: string; + readonly options: FirebaseOptions; + automaticDataCollectionEnabled: boolean; +}; + +export declare type FirebaseOptions = { + apiKey?: string; + authDomain?: string; + databaseURL?: string; + projectId?: string; + storageBucket?: string; + messagingSenderId?: string; + appId?: string; + measurementId?: string; +}; + +export type NotificationPayload = { + title?: string; + body?: string; + image?: string; + icon?: string; +}; + +export type FcmOptions = { + link?: string; + analyticsLabel?: string; +}; + +export type MessagePayload = { + notification?: NotificationPayload; + data?: { [key: string]: string }; + fcmOptions?: FcmOptions; + from: string; + collapseKey: string; + messageId: string; +}; + +export type GetTokenOptions = { + vapidKey?: string; + serviceWorkerRegistration?: ServiceWorkerRegistration; +}; diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts new file mode 100644 index 000000000000..2ffbe89fdd9e --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts @@ -0,0 +1,80 @@ +import { + formatAmount, + getAmount, + getLeadingZeroCount, +} from './get-notification-data'; + +describe('getNotificationData - formatAmount() tests', () => { + test('Should format large numbers', () => { + expect(formatAmount(1000)).toBe('1K'); + expect(formatAmount(1500)).toBe('1.5K'); + expect(formatAmount(1000000)).toBe('1M'); + expect(formatAmount(1000000000)).toBe('1B'); + expect(formatAmount(1000000000000)).toBe('1T'); + expect(formatAmount(1234567)).toBe('1.23M'); + }); + + test('Should format smaller numbers (<1000) with custom decimal place', () => { + const formatOptions = { decimalPlaces: 18 }; + expect(formatAmount(100.0012, formatOptions)).toBe('100.0012'); + expect(formatAmount(100.001200001, formatOptions)).toBe('100.001200001'); + expect(formatAmount(1e-18, formatOptions)).toBe('0.000000000000000001'); + expect(formatAmount(1e-19, formatOptions)).toBe('0'); // number is smaller than decimals given, hence 0 + }); + + test('Should format small numbers (<1000) up to 4 decimals otherwise uses ellipses', () => { + const formatOptions = { shouldEllipse: true }; + expect(formatAmount(100.1, formatOptions)).toBe('100.1'); + expect(formatAmount(100.01, formatOptions)).toBe('100.01'); + expect(formatAmount(100.001, formatOptions)).toBe('100.001'); + expect(formatAmount(100.0001, formatOptions)).toBe('100.0001'); + expect(formatAmount(100.00001, formatOptions)).toBe('100.0000...'); // since number is has >4 decimals, it will be truncated + expect(formatAmount(0.00001, formatOptions)).toBe('0.0000...'); // since number is has >4 decimals, it will be truncated + }); + + test('Should format small numbers (<1000) to custom decimal places and ellipse', () => { + const formatOptions = { decimalPlaces: 2, shouldEllipse: true }; + expect(formatAmount(100.1, formatOptions)).toBe('100.1'); + expect(formatAmount(100.01, formatOptions)).toBe('100.01'); + expect(formatAmount(100.001, formatOptions)).toBe('100.00...'); + expect(formatAmount(100.0001, formatOptions)).toBe('100.00...'); + expect(formatAmount(100.00001, formatOptions)).toBe('100.00...'); // since number is has >2 decimals, it will be truncated + expect(formatAmount(0.00001, formatOptions)).toBe('0.00...'); // since number is has >2 decimals, it will be truncated + }); +}); + +describe('getNotificationData - getAmount() tests', () => { + test('Should get formatted amount for larger numbers', () => { + expect(getAmount('1', '2')).toBe('0.01'); + expect(getAmount('10', '2')).toBe('0.1'); + expect(getAmount('100', '2')).toBe('1'); + expect(getAmount('1000', '2')).toBe('10'); + expect(getAmount('10000', '2')).toBe('100'); + expect(getAmount('100000', '2')).toBe('1K'); + expect(getAmount('1000000', '2')).toBe('10K'); + }); + test('Should get formatted amount for small/decimal numbers', () => { + const formatOptions = { shouldEllipse: true }; + expect(getAmount('100000', '5', formatOptions)).toBe('1'); + expect(getAmount('100001', '5', formatOptions)).toBe('1.0000...'); + expect(getAmount('10000', '5', formatOptions)).toBe('0.1'); + expect(getAmount('1000', '5', formatOptions)).toBe('0.01'); + expect(getAmount('100', '5', formatOptions)).toBe('0.001'); + expect(getAmount('10', '5', formatOptions)).toBe('0.0001'); + expect(getAmount('1', '5', formatOptions)).toBe('0.0000...'); + }); +}); + +describe('getNotificationData - getLeadingZeroCount() tests', () => { + test('Should handle all test cases', () => { + expect(getLeadingZeroCount(0)).toBe(0); + expect(getLeadingZeroCount(-1)).toBe(0); + expect(getLeadingZeroCount(1e-1)).toBe(0); + + expect(getLeadingZeroCount('1.01')).toBe(1); + expect(getLeadingZeroCount('3e-2')).toBe(1); + expect(getLeadingZeroCount('100.001e1')).toBe(1); + + expect(getLeadingZeroCount('0.00120043')).toBe(2); + }); +}); diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts new file mode 100644 index 000000000000..f95149c54c19 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts @@ -0,0 +1,90 @@ +import { BigNumber } from 'bignumber.js'; +import { calcTokenAmount } from '../../../../../shared/lib/transactions-controller-utils'; + +type FormatOptions = { + decimalPlaces?: number; + shouldEllipse?: boolean; +}; +const defaultFormatOptions = { + decimalPlaces: 4, +}; + +/** + * Calculates the number of leading zeros in the fractional part of a number. + * + * This function converts a number or a string representation of a number into + * its decimal form and then counts the number of leading zeros present in the + * fractional part of the number. This is useful for determining the precision + * of very small numbers. + * + * @param num - The number to analyze, which can be in the form + * of a number or a string. + * @returns The count of leading zeros in the fractional part of the number. + */ +export const getLeadingZeroCount = (num: number | string) => { + const numToString = new BigNumber(num, 10).toString(10); + const fractionalPart = numToString.split('.')[1] ?? ''; + return fractionalPart.match(/^0*/u)?.[0]?.length || 0; +}; + +/** + * This formats a number using Intl + * It abbreviates large numbers (using K, M, B, T) + * And abbreviates small numbers in 2 ways: + * - Will format to the given number of decimal places + * - Will format up to 4 decimal places + * - Will ellipse the number if longer than given decimal places + * + * @param numericAmount - The number to format + * @param opts - The options to use when formatting + * @returns The formatted number + */ +export const formatAmount = (numericAmount: number, opts?: FormatOptions) => { + // create options with defaults + const options = { ...defaultFormatOptions, ...opts }; + + const leadingZeros = getLeadingZeroCount(numericAmount); + const isDecimal = numericAmount.toString().includes('.') || leadingZeros > 0; + const isLargeNumber = numericAmount > 999; + + const handleShouldEllipse = (decimalPlaces: number) => + Boolean(options?.shouldEllipse) && leadingZeros >= decimalPlaces; + + if (isLargeNumber) { + return Intl.NumberFormat('en-US', { + notation: 'compact', + compactDisplay: 'short', + maximumFractionDigits: 2, + }).format(numericAmount); + } + + if (isDecimal) { + const ellipse = handleShouldEllipse(options.decimalPlaces); + const formattedValue = Intl.NumberFormat('en-US', { + minimumFractionDigits: ellipse ? options.decimalPlaces : undefined, + maximumFractionDigits: options.decimalPlaces, + }).format(numericAmount); + + return ellipse ? `${formattedValue}...` : formattedValue; + } + + // Default to showing the raw amount + return numericAmount.toString(); +}; + +export const getAmount = ( + amount: string, + decimals: string, + options?: FormatOptions, +) => { + if (!amount || !decimals) { + return ''; + } + + const numericAmount = calcTokenAmount( + amount, + parseFloat(decimals), + ).toNumber(); + + return formatAmount(numericAmount, options); +}; diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts new file mode 100644 index 000000000000..75f65d822467 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts @@ -0,0 +1,150 @@ +import { + createMockNotificationERC1155Received, + createMockNotificationERC1155Sent, + createMockNotificationERC20Received, + createMockNotificationERC20Sent, + createMockNotificationERC721Received, + createMockNotificationERC721Sent, + createMockNotificationEthReceived, + createMockNotificationEthSent, + createMockNotificationLidoReadyToBeWithdrawn, + createMockNotificationLidoStakeCompleted, + createMockNotificationLidoWithdrawalCompleted, + createMockNotificationLidoWithdrawalRequested, + createMockNotificationMetaMaskSwapsCompleted, + createMockNotificationRocketPoolStakeCompleted, + createMockNotificationRocketPoolUnStakeCompleted, +} from '../../metamask-notifications/mocks/mock-raw-notifications'; +import { createNotificationMessage } from './get-notification-message'; + +describe('notification-message tests', () => { + test('displays erc20 sent notification', () => { + const notification = createMockNotificationERC20Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds sent'); + expect(result?.description).toContain('You successfully sent 4.96K USDC'); + }); + + test('displays erc20 received notification', () => { + const notification = createMockNotificationERC20Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds received'); + expect(result?.description).toContain('You received 8.38B SHIB'); + }); + + test('displays eth/native sent notification', () => { + const notification = createMockNotificationEthSent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds sent'); + expect(result?.description).toContain('You successfully sent 0.005 ETH'); + }); + + test('displays eth/native received notification', () => { + const notification = createMockNotificationEthReceived(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds received'); + expect(result?.description).toContain('You received 808 ETH'); + }); + + test('displays metamask swap completed notification', () => { + const notification = createMockNotificationMetaMaskSwapsCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Swap completed'); + expect(result?.description).toContain('Your MetaMask Swap was successful'); + }); + + test('displays erc721 sent notification', () => { + const notification = createMockNotificationERC721Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT sent'); + expect(result?.description).toContain('You have successfully sent an NFT'); + }); + + test('displays erc721 received notification', () => { + const notification = createMockNotificationERC721Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT received'); + expect(result?.description).toContain('You received new NFTs'); + }); + + test('displays erc1155 sent notification', () => { + const notification = createMockNotificationERC1155Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT sent'); + expect(result?.description).toContain('You have successfully sent an NFT'); + }); + + test('displays erc1155 received notification', () => { + const notification = createMockNotificationERC1155Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT received'); + expect(result?.description).toContain('You received new NFTs'); + }); + + test('displays rocketpool stake completed notification', () => { + const notification = createMockNotificationRocketPoolStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake complete'); + expect(result?.description).toContain( + 'Your RocketPool stake was successful', + ); + }); + + test('displays rocketpool unstake completed notification', () => { + const notification = createMockNotificationRocketPoolUnStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Unstake complete'); + expect(result?.description).toContain( + 'Your RocketPool unstake was successful', + ); + }); + + test('displays lido stake completed notification', () => { + const notification = createMockNotificationLidoStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake complete'); + expect(result?.description).toContain('Your Lido stake was successful'); + }); + + test('displays lido stake ready to be withdrawn notification', () => { + const notification = createMockNotificationLidoReadyToBeWithdrawn(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake ready for withdrawal'); + expect(result?.description).toContain( + 'Your Lido stake is now ready to be withdrawn', + ); + }); + + test('displays lido withdrawal requested notification', () => { + const notification = createMockNotificationLidoWithdrawalRequested(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Withdrawal requested'); + expect(result?.description).toContain( + 'Your Lido withdrawal request was submitted', + ); + }); + + test('displays lido withdrawal completed notification', () => { + const notification = createMockNotificationLidoWithdrawalCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Withdrawal completed'); + expect(result?.description).toContain( + 'Your Lido withdrawal was successful', + ); + }); +}); diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts new file mode 100644 index 000000000000..9fb296b51b4d --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts @@ -0,0 +1,246 @@ +// We are defining that this file uses a webworker global scope. +// eslint-disable-next-line spaced-comment +/// + +import { CHAIN_SYMBOLS } from '../../metamask-notifications/constants/notification-schema'; +import type { TRIGGER_TYPES } from '../../metamask-notifications/constants/notification-schema'; +import type { OnChainRawNotification } from '../../metamask-notifications/types/on-chain-notification/on-chain-notification'; +import { t } from '../../../translate'; +import { getAmount, formatAmount } from './get-notification-data'; + +type PushNotificationMessage = { + title: string; + description: string; +}; + +type NotificationMessage< + N extends OnChainRawNotification = OnChainRawNotification, +> = { + title: string | null; + defaultDescription: string | null; + getDescription?: (n: N) => string | null; +}; + +type NotificationMessageDict = { + [K in TRIGGER_TYPES]?: NotificationMessage< + Extract + >; +}; + +const sw = self as unknown as ServiceWorkerGlobalScope; + +function getChainSymbol(chainId: number) { + return CHAIN_SYMBOLS[chainId] ?? null; +} + +export async function onPushNotification(notification: unknown): Promise { + if (!notification) { + return; + } + if (!isOnChainNotification(notification)) { + return; + } + + const notificationMessage = createNotificationMessage(notification); + if (!notificationMessage) { + return; + } + + const registration = sw?.registration; + if (!registration) { + return; + } + + await registration.showNotification(notificationMessage.title, { + body: notificationMessage.description, + icon: './images/icon-64.png', + tag: notification?.id, + data: notification, + }); +} + +function isOnChainNotification(n: unknown): n is OnChainRawNotification { + const assumed = n as OnChainRawNotification; + + // We don't have a validation/parsing library to check all possible types of an on chain notification + // It is safe enough just to check "some" fields, and catch any errors down the line if the shape is bad. + const isValidEnoughToBeOnChainNotification = [ + assumed?.id, + assumed?.data, + assumed?.trigger_id, + ].every((field) => field !== undefined); + return isValidEnoughToBeOnChainNotification; +} + +const notificationMessageDict: NotificationMessageDict = { + erc20_sent: { + title: t('pushPlatformNotificationsFundsSentTitle'), + defaultDescription: t( + 'pushPlatformNotificationsFundsSentDescriptionDefault', + ), + getDescription: (n) => { + const symbol = n?.data?.token?.symbol; + const tokenAmount = n?.data?.token?.amount; + const tokenDecimals = n?.data?.token?.decimals; + if (!symbol || !tokenAmount || !tokenDecimals) { + return null; + } + + const amount = getAmount(tokenAmount, tokenDecimals, { + shouldEllipse: true, + }); + return t('pushPlatformNotificationsFundsSentDescription', amount, symbol); + }, + }, + eth_sent: { + title: t('pushPlatformNotificationsFundsSentTitle'), + defaultDescription: t( + 'pushPlatformNotificationsFundsSentDescriptionDefault', + ), + getDescription: (n) => { + const symbol = getChainSymbol(n?.chain_id); + const tokenAmount = n?.data?.amount?.eth; + if (!symbol || !tokenAmount) { + return null; + } + + const amount = formatAmount(parseFloat(tokenAmount), { + shouldEllipse: true, + }); + return t('pushPlatformNotificationsFundsSentDescription', amount, symbol); + }, + }, + erc20_received: { + title: t('pushPlatformNotificationsFundsReceivedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsFundsReceivedDescriptionDefault', + ), + getDescription: (n) => { + const symbol = n?.data?.token?.symbol; + const tokenAmount = n?.data?.token?.amount; + const tokenDecimals = n?.data?.token?.decimals; + if (!symbol || !tokenAmount || !tokenDecimals) { + return null; + } + + const amount = getAmount(tokenAmount, tokenDecimals, { + shouldEllipse: true, + }); + return t( + 'pushPlatformNotificationsFundsReceivedDescription', + amount, + symbol, + ); + }, + }, + eth_received: { + title: t('pushPlatformNotificationsFundsReceivedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsFundsReceivedDescriptionDefault', + ), + getDescription: (n) => { + const symbol = getChainSymbol(n?.chain_id); + const tokenAmount = n?.data?.amount?.eth; + if (!symbol || !tokenAmount) { + return null; + } + + const amount = formatAmount(parseFloat(tokenAmount), { + shouldEllipse: true, + }); + return t( + 'pushPlatformNotificationsFundsReceivedDescription', + amount, + symbol, + ); + }, + }, + metamask_swap_completed: { + title: t('pushPlatformNotificationsSwapCompletedTitle'), + defaultDescription: t('pushPlatformNotificationsSwapCompletedDescription'), + }, + erc721_sent: { + title: t('pushPlatformNotificationsNftSentTitle'), + defaultDescription: t('pushPlatformNotificationsNftSentDescription'), + }, + erc1155_sent: { + title: t('pushPlatformNotificationsNftSentTitle'), + defaultDescription: t('pushPlatformNotificationsNftSentDescription'), + }, + erc721_received: { + title: t('pushPlatformNotificationsNftReceivedTitle'), + defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), + }, + erc1155_received: { + title: t('pushPlatformNotificationsNftReceivedTitle'), + defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), + }, + rocketpool_stake_completed: { + title: t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription', + ), + }, + rocketpool_unstake_completed: { + title: t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', + ), + }, + lido_stake_completed: { + title: t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsStakingLidoStakeCompletedDescription', + ), + }, + lido_stake_ready_to_be_withdrawn: { + title: t( + 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle', + ), + defaultDescription: t( + 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', + ), + }, + lido_withdrawal_requested: { + title: t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription', + ), + }, + lido_withdrawal_completed: { + title: t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), + defaultDescription: t( + 'pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription', + ), + }, +}; + +export function createNotificationMessage( + n: OnChainRawNotification, +): PushNotificationMessage | null { + if (!n?.data?.kind) { + return null; + } + const notificationMessage = notificationMessageDict[n.data.kind] as + | NotificationMessage + | undefined; + + if (!notificationMessage) { + return null; + } + + let description: string | null = null; + try { + description = + notificationMessage?.getDescription?.(n) ?? + notificationMessage.defaultDescription ?? + null; + } catch (e) { + description = notificationMessage.defaultDescription ?? null; + } + + return { + title: notificationMessage.title ?? '', // Ensure title is always a string + description: description ?? '', // Fallback to empty string if null + }; +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 370ec5b220a0..488cd52d1ebc 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -320,6 +320,8 @@ import AuthenticationController from './controllers/authentication/authenticatio import UserStorageController from './controllers/user-storage/user-storage-controller'; import { WeakRefObjectMap } from './lib/WeakRefObjectMap'; +import { PushPlatformNotificationsController } from './controllers/push-platform-notifications/push-platform-notifications'; + export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) // The process of updating the badge happens in app/scripts/background.js. @@ -1433,6 +1435,14 @@ export default class MetamaskController extends EventEmitter { ], }), }); + this.pushPlatformNotificationsController = + new PushPlatformNotificationsController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'PushPlatformNotificationsController', + allowedActions: ['AuthenticationController:getBearerToken'], + }), + state: initState.PushPlatformNotificationsController, + }); // account tracker watches balances, nonces, and any code at their address this.accountTracker = new AccountTracker({ diff --git a/builds.yml b/builds.yml index c19a86aafc72..188e1cfe2249 100644 --- a/builds.yml +++ b/builds.yml @@ -212,6 +212,16 @@ env: - CONTENTFUL_ACCESS_TOKEN: null - NOTIFICATIONS_SERVICE_URL: https://notification.api.cx.metamask.io - TRIGGERS_SERVICE_URL: https://trigger.api.cx.metamask.io + - PUSH_NOTIFICATIONS_SERVICE_URL: https://push.api.cx.metamask.io + - VAPID_KEY: null + - FIREBASE_API_KEY: null + - FIREBASE_AUTH_DOMAIN: null + - FIREBASE_STORAGE_BUCKET: null + - FIREBASE_PROJECT_ID: null + - FIREBASE_MESSAGING_SENDER_ID: null + - FIREBASE_APP_ID: null + - FIREBASE_MEASUREMENT_ID: null + - __FIREBASE_DEFAULTS__: null ### # API keys to 3rd party services diff --git a/development/build/scripts.js b/development/build/scripts.js index 476e4974df71..01973393f566 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -894,6 +894,14 @@ function setupBundlerDefaults( extensions, }, ], + // We are transpelling the firebase package to be compatible with the lavaMoat restrictions + [ + babelify, + { + only: ['./**/node_modules/firebase', './**/node_modules/@firebase'], + global: true, + }, + ], ], // Look for TypeScript files when walking the dependency tree extensions, diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js index 2a1e73cb8557..fd679815d52f 100755 --- a/development/verify-locale-strings.js +++ b/development/verify-locale-strings.js @@ -192,6 +192,7 @@ async function verifyEnglishLocale() { 'app/scripts/constants/**/*.js', 'app/scripts/constants/**/*.ts', 'app/scripts/platforms/**/*.js', + 'app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts', ], { ignore: [...globsToStrictSearch, testGlob], diff --git a/jest.config.js b/jest.config.js index ba767d8f2190..18204323087c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -52,6 +52,7 @@ module.exports = { '/app/scripts/controllers/sign.test.ts', '/app/scripts/controllers/decrypt-message.test.ts', '/app/scripts/controllers/authentication/**/*.test.ts', + '/app/scripts/controllers/push-platform-notifications/**/*.test.ts', '/app/scripts/controllers/user-storage/**/*.test.ts', '/app/scripts/controllers/metamask-notifications/**/*.test.ts', '/app/scripts/flask/**/*.test.js', diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 4d333a7cfe6d..dedfc21ec8c6 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -3720,6 +3720,121 @@ "setTimeout": true } }, + "firebase": { + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/messaging": true + } + }, + "firebase>@firebase/app": { + "globals": { + "FinalizationRegistry": true, + "console.warn": true + }, + "packages": { + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>@firebase/logger": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/component": { + "packages": { + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/logger": { + "globals": { + "console": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, + "firebase>@firebase/app>idb": { + "globals": { + "DOMException": true, + "IDBCursor": true, + "IDBDatabase": true, + "IDBIndex": true, + "IDBObjectStore": true, + "IDBRequest": true, + "IDBTransaction": true, + "indexedDB.deleteDatabase": true, + "indexedDB.open": true + } + }, + "firebase>@firebase/installations": { + "globals": { + "BroadcastChannel": true, + "Headers": true, + "btoa": true, + "console.error": true, + "crypto": true, + "fetch": true, + "msCrypto": true, + "navigator.onLine": true, + "setTimeout": true + }, + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/messaging": { + "globals": { + "Headers": true, + "Notification.maxActions": true, + "Notification.permission": true, + "Notification.requestPermission": true, + "PushSubscription.prototype.hasOwnProperty": true, + "ServiceWorkerRegistration": true, + "URL": true, + "addEventListener": true, + "atob": true, + "btoa": true, + "clients.matchAll": true, + "clients.openWindow": true, + "console.warn": true, + "document": true, + "fetch": true, + "indexedDB": true, + "location.href": true, + "location.origin": true, + "navigator": true, + "origin.replace": true, + "registration.showNotification": true, + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true, + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/installations": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/util": { + "globals": { + "atob": true, + "browser": true, + "btoa": true, + "chrome": true, + "console": true, + "document": true, + "indexedDB": true, + "navigator": true, + "process": true, + "self": true, + "setTimeout": true + }, + "packages": { + "browserify>process": true + } + }, "fuse.js": { "globals": { "console": true, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 887d53245993..bfb6caffb96c 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -4044,6 +4044,121 @@ "setTimeout": true } }, + "firebase": { + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/messaging": true + } + }, + "firebase>@firebase/app": { + "globals": { + "FinalizationRegistry": true, + "console.warn": true + }, + "packages": { + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>@firebase/logger": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/component": { + "packages": { + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/logger": { + "globals": { + "console": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, + "firebase>@firebase/app>idb": { + "globals": { + "DOMException": true, + "IDBCursor": true, + "IDBDatabase": true, + "IDBIndex": true, + "IDBObjectStore": true, + "IDBRequest": true, + "IDBTransaction": true, + "indexedDB.deleteDatabase": true, + "indexedDB.open": true + } + }, + "firebase>@firebase/installations": { + "globals": { + "BroadcastChannel": true, + "Headers": true, + "btoa": true, + "console.error": true, + "crypto": true, + "fetch": true, + "msCrypto": true, + "navigator.onLine": true, + "setTimeout": true + }, + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/messaging": { + "globals": { + "Headers": true, + "Notification.maxActions": true, + "Notification.permission": true, + "Notification.requestPermission": true, + "PushSubscription.prototype.hasOwnProperty": true, + "ServiceWorkerRegistration": true, + "URL": true, + "addEventListener": true, + "atob": true, + "btoa": true, + "clients.matchAll": true, + "clients.openWindow": true, + "console.warn": true, + "document": true, + "fetch": true, + "indexedDB": true, + "location.href": true, + "location.origin": true, + "navigator": true, + "origin.replace": true, + "registration.showNotification": true, + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true, + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/installations": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/util": { + "globals": { + "atob": true, + "browser": true, + "btoa": true, + "chrome": true, + "console": true, + "document": true, + "indexedDB": true, + "navigator": true, + "process": true, + "self": true, + "setTimeout": true + }, + "packages": { + "browserify>process": true + } + }, "fuse.js": { "globals": { "console": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index c80a3c1edd75..3ccfb816f5ca 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -4096,6 +4096,121 @@ "setTimeout": true } }, + "firebase": { + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/messaging": true + } + }, + "firebase>@firebase/app": { + "globals": { + "FinalizationRegistry": true, + "console.warn": true + }, + "packages": { + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>@firebase/logger": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/component": { + "packages": { + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/logger": { + "globals": { + "console": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, + "firebase>@firebase/app>idb": { + "globals": { + "DOMException": true, + "IDBCursor": true, + "IDBDatabase": true, + "IDBIndex": true, + "IDBObjectStore": true, + "IDBRequest": true, + "IDBTransaction": true, + "indexedDB.deleteDatabase": true, + "indexedDB.open": true + } + }, + "firebase>@firebase/installations": { + "globals": { + "BroadcastChannel": true, + "Headers": true, + "btoa": true, + "console.error": true, + "crypto": true, + "fetch": true, + "msCrypto": true, + "navigator.onLine": true, + "setTimeout": true + }, + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/messaging": { + "globals": { + "Headers": true, + "Notification.maxActions": true, + "Notification.permission": true, + "Notification.requestPermission": true, + "PushSubscription.prototype.hasOwnProperty": true, + "ServiceWorkerRegistration": true, + "URL": true, + "addEventListener": true, + "atob": true, + "btoa": true, + "clients.matchAll": true, + "clients.openWindow": true, + "console.warn": true, + "document": true, + "fetch": true, + "indexedDB": true, + "location.href": true, + "location.origin": true, + "navigator": true, + "origin.replace": true, + "registration.showNotification": true, + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true, + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/installations": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/util": { + "globals": { + "atob": true, + "browser": true, + "btoa": true, + "chrome": true, + "console": true, + "document": true, + "indexedDB": true, + "navigator": true, + "process": true, + "self": true, + "setTimeout": true + }, + "packages": { + "browserify>process": true + } + }, "fuse.js": { "globals": { "console": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 689e41098d34..18101c56499d 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -4011,6 +4011,121 @@ "setTimeout": true } }, + "firebase": { + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/messaging": true + } + }, + "firebase>@firebase/app": { + "globals": { + "FinalizationRegistry": true, + "console.warn": true + }, + "packages": { + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>@firebase/logger": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/component": { + "packages": { + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/logger": { + "globals": { + "console": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, + "firebase>@firebase/app>idb": { + "globals": { + "DOMException": true, + "IDBCursor": true, + "IDBDatabase": true, + "IDBIndex": true, + "IDBObjectStore": true, + "IDBRequest": true, + "IDBTransaction": true, + "indexedDB.deleteDatabase": true, + "indexedDB.open": true + } + }, + "firebase>@firebase/installations": { + "globals": { + "BroadcastChannel": true, + "Headers": true, + "btoa": true, + "console.error": true, + "crypto": true, + "fetch": true, + "msCrypto": true, + "navigator.onLine": true, + "setTimeout": true + }, + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/messaging": { + "globals": { + "Headers": true, + "Notification.maxActions": true, + "Notification.permission": true, + "Notification.requestPermission": true, + "PushSubscription.prototype.hasOwnProperty": true, + "ServiceWorkerRegistration": true, + "URL": true, + "addEventListener": true, + "atob": true, + "btoa": true, + "clients.matchAll": true, + "clients.openWindow": true, + "console.warn": true, + "document": true, + "fetch": true, + "indexedDB": true, + "location.href": true, + "location.origin": true, + "navigator": true, + "origin.replace": true, + "registration.showNotification": true, + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true, + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/installations": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/util": { + "globals": { + "atob": true, + "browser": true, + "btoa": true, + "chrome": true, + "console": true, + "document": true, + "indexedDB": true, + "navigator": true, + "process": true, + "self": true, + "setTimeout": true + }, + "packages": { + "browserify>process": true + } + }, "fuse.js": { "globals": { "console": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 1272c1f25a8e..48c41aa34618 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -4150,6 +4150,121 @@ "setTimeout": true } }, + "firebase": { + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/messaging": true + } + }, + "firebase>@firebase/app": { + "globals": { + "FinalizationRegistry": true, + "console.warn": true + }, + "packages": { + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>@firebase/logger": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/component": { + "packages": { + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/app>@firebase/logger": { + "globals": { + "console": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, + "firebase>@firebase/app>idb": { + "globals": { + "DOMException": true, + "IDBCursor": true, + "IDBDatabase": true, + "IDBIndex": true, + "IDBObjectStore": true, + "IDBRequest": true, + "IDBTransaction": true, + "indexedDB.deleteDatabase": true, + "indexedDB.open": true + } + }, + "firebase>@firebase/installations": { + "globals": { + "BroadcastChannel": true, + "Headers": true, + "btoa": true, + "console.error": true, + "crypto": true, + "fetch": true, + "msCrypto": true, + "navigator.onLine": true, + "setTimeout": true + }, + "packages": { + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/messaging": { + "globals": { + "Headers": true, + "Notification.maxActions": true, + "Notification.permission": true, + "Notification.requestPermission": true, + "PushSubscription.prototype.hasOwnProperty": true, + "ServiceWorkerRegistration": true, + "URL": true, + "addEventListener": true, + "atob": true, + "btoa": true, + "clients.matchAll": true, + "clients.openWindow": true, + "console.warn": true, + "document": true, + "fetch": true, + "indexedDB": true, + "location.href": true, + "location.origin": true, + "navigator": true, + "origin.replace": true, + "registration.showNotification": true, + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true, + "firebase>@firebase/app": true, + "firebase>@firebase/app>@firebase/component": true, + "firebase>@firebase/app>idb": true, + "firebase>@firebase/installations": true, + "firebase>@firebase/util": true + } + }, + "firebase>@firebase/util": { + "globals": { + "atob": true, + "browser": true, + "btoa": true, + "chrome": true, + "console": true, + "document": true, + "indexedDB": true, + "navigator": true, + "process": true, + "self": true, + "setTimeout": true + }, + "packages": { + "browserify>process": true + } + }, "fuse.js": { "globals": { "console": true, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 2edc817ef288..3aa9214aaf0c 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -3167,6 +3167,30 @@ "chokidar>braces": true } }, + "firebase>@firebase/database>faye-websocket>websocket-driver": { + "builtin": { + "crypto.createHash": true, + "crypto.randomBytes": true, + "events.EventEmitter": true, + "stream.Stream": true, + "url.parse": true, + "util.inherits": true + }, + "globals": { + "Buffer": true, + "process.version.match": true + }, + "packages": { + "firebase>@firebase/database>faye-websocket>websocket-driver>http-parser-js": true, + "firebase>@firebase/database>faye-websocket>websocket-driver>websocket-extensions": true + } + }, + "firebase>@firebase/database>faye-websocket>websocket-driver>http-parser-js": { + "builtin": { + "assert.equal": true, + "assert.ok": true + } + }, "fs-extra": { "builtin": { "assert": true, @@ -3497,31 +3521,7 @@ "setInterval": true }, "packages": { - "gulp-livereload>tiny-lr>faye-websocket>websocket-driver": true - } - }, - "gulp-livereload>tiny-lr>faye-websocket>websocket-driver": { - "builtin": { - "crypto.createHash": true, - "crypto.randomBytes": true, - "events.EventEmitter": true, - "stream.Stream": true, - "url.parse": true, - "util.inherits": true - }, - "globals": { - "Buffer": true, - "process.version.match": true - }, - "packages": { - "gulp-livereload>tiny-lr>faye-websocket>websocket-driver>http-parser-js": true, - "gulp-livereload>tiny-lr>faye-websocket>websocket-driver>websocket-extensions": true - } - }, - "gulp-livereload>tiny-lr>faye-websocket>websocket-driver>http-parser-js": { - "builtin": { - "assert.equal": true, - "assert.ok": true + "firebase>@firebase/database>faye-websocket>websocket-driver": true } }, "gulp-postcss": { @@ -5939,8 +5939,8 @@ "console.log": true }, "packages": { - "@babel/core>@babel/parser": true, - "depcheck>@babel/traverse": true + "depcheck>@babel/traverse": true, + "lavamoat-viz>lavamoat-core>lavamoat-tofu>@babel/parser": true } }, "lavamoat>@lavamoat/aa": { diff --git a/package.json b/package.json index 1b47d0f7b954..feb7053b2903 100644 --- a/package.json +++ b/package.json @@ -364,6 +364,7 @@ "ethereumjs-util": "^7.0.10", "extension-port-stream": "^3.0.0", "fast-json-patch": "^3.1.1", + "firebase": "^10.11.0", "fuse.js": "^3.2.0", "human-standard-token-abi": "^2.0.0", "immer": "^9.0.6", @@ -683,7 +684,8 @@ "@trezor/connect-web>@trezor/connect>@trezor/utxo-lib>tiny-secp256k1": false, "@storybook/test-runner>@swc/core": false, "@lavamoat/lavadome-react>@lavamoat/preinstall-always-fail": false, - "tsx>esbuild": false + "tsx>esbuild": false, + "firebase>@firebase/firestore>@grpc/proto-loader>protobufjs": false } }, "packageManager": "yarn@4.0.2" diff --git a/yarn.lock b/yarn.lock index 62c06c0bf54c..e2101682f3c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -462,7 +462,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:7.24.0, @babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0": +"@babel/parser@npm:7.24.0": version: 7.24.0 resolution: "@babel/parser@npm:7.24.0" bin: @@ -471,6 +471,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.13.9, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.22.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0": + version: 7.24.4 + resolution: "@babel/parser@npm:7.24.4" + bin: + parser: ./bin/babel-parser.js + checksum: 3742cc5068036287e6395269dce5a2735e6349cdc8d4b53297c75f98c580d7e1c8cb43235623999d151f2ef975d677dbc2c2357573a1855caa71c271bf3046c9 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": version: 7.22.15 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" @@ -2871,6 +2880,13 @@ __metadata: languageName: node linkType: hard +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 + languageName: node + linkType: hard + "@figspec/components@npm:^1.0.1": version: 1.0.2 resolution: "@figspec/components@npm:1.0.2" @@ -2892,6 +2908,518 @@ __metadata: languageName: node linkType: hard +"@firebase/analytics-compat@npm:0.2.8": + version: 0.2.8 + resolution: "@firebase/analytics-compat@npm:0.2.8" + dependencies: + "@firebase/analytics": "npm:0.10.2" + "@firebase/analytics-types": "npm:0.8.1" + "@firebase/component": "npm:0.6.6" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: bd26bd9ce5ef692394cf7ce21c8d5375ceb90614aee1d5bdb31910b8eff0844f8f812a3c8ba551ef4848f6d6bbbcb68e47b350967afa3fbf51e524c2b1c6865a + languageName: node + linkType: hard + +"@firebase/analytics-types@npm:0.8.1": + version: 0.8.1 + resolution: "@firebase/analytics-types@npm:0.8.1" + checksum: 934e61faef523b8d064f1801d70136eb8a3e4ae68c37fe7273be0e8aad1694965a5f4a23956ab25fa232b20f7c151fa7dba709b2a0c3593de374db605c26187c + languageName: node + linkType: hard + +"@firebase/analytics@npm:0.10.2": + version: 0.10.2 + resolution: "@firebase/analytics@npm:0.10.2" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/installations": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 841c5350ccaab26283646aba5b1c48082a3694a1a47570a7c6a3845f2a69bc1f43355da611bf9fb78070ba709ddf843d9c8dd94a13697dbcc5a985eda3ef2f8c + languageName: node + linkType: hard + +"@firebase/app-check-compat@npm:0.3.10": + version: 0.3.10 + resolution: "@firebase/app-check-compat@npm:0.3.10" + dependencies: + "@firebase/app-check": "npm:0.8.3" + "@firebase/app-check-types": "npm:0.5.1" + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 7f369459edf76d4aef9d7bb0660e5cddbe8cfa85ef51adba5b78c68fc8bac5f1762c3c6f1d8e1f46d764d028387dcff0eff729042ccb7a61c4c0ec5b63e41b90 + languageName: node + linkType: hard + +"@firebase/app-check-interop-types@npm:0.3.1": + version: 0.3.1 + resolution: "@firebase/app-check-interop-types@npm:0.3.1" + checksum: a93415e4acb0896c9fe99748d01303369d133e468ca97ef3021c54b4fd7aec16951adfcba0c9ff4b6e7e951fcb3c62d1ad9864516a37ca77d5120beebcec6994 + languageName: node + linkType: hard + +"@firebase/app-check-types@npm:0.5.1": + version: 0.5.1 + resolution: "@firebase/app-check-types@npm:0.5.1" + checksum: 5c546dae2f0ed6c3cac7896517d28ee224f7a4b6dc24ecb8eb14ffac983a551ae72aa1fea549f0786e70b346e219c700a6ddd811abbdb532679f8447da6db5b3 + languageName: node + linkType: hard + +"@firebase/app-check@npm:0.8.3": + version: 0.8.3 + resolution: "@firebase/app-check@npm:0.8.3" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: a64f947d5665512b3a3df285dc2912773499fd20e0caf50681e9d485511a19a333b7554034612c40dc66af0e0cd8592258a770ff2d3cd69f18c1f0f56062a416 + languageName: node + linkType: hard + +"@firebase/app-compat@npm:0.2.31": + version: 0.2.31 + resolution: "@firebase/app-compat@npm:0.2.31" + dependencies: + "@firebase/app": "npm:0.10.1" + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + checksum: 969b82047985b95dbb76993e97723fae43a04b3191af09f477674a3c39960f8473b18e2ff5dde397d6aa495f0d47445451e7833834ce875b46afc06049b4a8f6 + languageName: node + linkType: hard + +"@firebase/app-types@npm:0.9.1": + version: 0.9.1 + resolution: "@firebase/app-types@npm:0.9.1" + checksum: 98f53967113b8d442030da7904056ee344e1e0ac273889e985d6cbff0122632e7e6f02a5736aedfbf4563b4d2f60e4c930c0e3328c895d6a62983b2b4e7c81f8 + languageName: node + linkType: hard + +"@firebase/app@npm:0.10.1": + version: 0.10.1 + resolution: "@firebase/app@npm:0.10.1" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + checksum: 09ce322d200e65a05f0e386def2078c7aa856cf3dbd4ddc897c982128925fef40aa0024378a271e562686fbe753bb98043500d73cb6c7312ad9e8e6919e071af + languageName: node + linkType: hard + +"@firebase/auth-compat@npm:0.5.6": + version: 0.5.6 + resolution: "@firebase/auth-compat@npm:0.5.6" + dependencies: + "@firebase/auth": "npm:1.7.1" + "@firebase/auth-types": "npm:0.12.1" + "@firebase/component": "npm:0.6.6" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + undici: "npm:5.28.4" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: c521f86cde926d9592ac7d3d8395c5c1ff8dd601eaa28ec36598380751e2089070d6e289242841adb05daa57c6b32ca4707fb0d8f0722e7c5df374878c1585cf + languageName: node + linkType: hard + +"@firebase/auth-interop-types@npm:0.2.2": + version: 0.2.2 + resolution: "@firebase/auth-interop-types@npm:0.2.2" + checksum: 900fe48dc41fd63dfe1e0a2b49ab2046b0f613beb0b56d2bd3fb59a6b6b4a6623f68be262a73bee46a1b28f6eec4a55307015b7169c2428acee4be024c14d4f0 + languageName: node + linkType: hard + +"@firebase/auth-types@npm:0.12.1": + version: 0.12.1 + resolution: "@firebase/auth-types@npm:0.12.1" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 4db2b3eaf9efcfe985fa14441e74b5b7bd5123aa0f1fcf8aa133eb5502526f62984a5d62488cd0faac1c63ca677b1a3ca05bc5f86154a54fe734d69a72846f85 + languageName: node + linkType: hard + +"@firebase/auth@npm:1.7.1": + version: 1.7.1 + resolution: "@firebase/auth@npm:1.7.1" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + undici: "npm:5.28.4" + peerDependencies: + "@firebase/app": 0.x + "@react-native-async-storage/async-storage": ^1.18.1 + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + checksum: ef728af26deadd4dee160f33684f2e703f3683258d113c9a97d932f626a2597efdf932d978edc7c98b21acbe9a2d6e98eb582d233cfb8aa5425e18f78bf308a1 + languageName: node + linkType: hard + +"@firebase/component@npm:0.6.6": + version: 0.6.6 + resolution: "@firebase/component@npm:0.6.6" + dependencies: + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + checksum: 963931307f36b2ce612c2e2caed50690e483863d496cf46ef268205abf6146f5d25ba04b536f1aeca8d3e434f9c1ca6bb8c2bdeac807bf2b290a3432a74f6360 + languageName: node + linkType: hard + +"@firebase/database-compat@npm:1.0.4": + version: 1.0.4 + resolution: "@firebase/database-compat@npm:1.0.4" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/database": "npm:1.0.4" + "@firebase/database-types": "npm:1.0.2" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + checksum: f55bec1a55d7de6efb2c49e63d698fb236c0fbb0610ad8da22d1d2e06e8335c3353863c03d9baceb26d506e99737b1974466b205bc53715efad7adf5b0b1ac06 + languageName: node + linkType: hard + +"@firebase/database-types@npm:1.0.2": + version: 1.0.2 + resolution: "@firebase/database-types@npm:1.0.2" + dependencies: + "@firebase/app-types": "npm:0.9.1" + "@firebase/util": "npm:1.9.5" + checksum: 426615cd62db18ba6a19baac29f828eb50e59e55a427f5ccb5d4121f560abfd837d8163a2594459d0c6abc4444cd4f952377b26c0ab4d981e0b86e4378eb688e + languageName: node + linkType: hard + +"@firebase/database@npm:1.0.4": + version: 1.0.4 + resolution: "@firebase/database@npm:1.0.4" + dependencies: + "@firebase/app-check-interop-types": "npm:0.3.1" + "@firebase/auth-interop-types": "npm:0.2.2" + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + faye-websocket: "npm:0.11.4" + tslib: "npm:^2.1.0" + checksum: 92593ee52aa353758a01f35f7fcfb3121fee633c28c77dd042e53375fc4ce60fdffd0f2ce2accc6f85ab7a6c482dd688c7c9fa208561afa9fa013b1c2cebf328 + languageName: node + linkType: hard + +"@firebase/firestore-compat@npm:0.3.29": + version: 0.3.29 + resolution: "@firebase/firestore-compat@npm:0.3.29" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/firestore": "npm:4.6.0" + "@firebase/firestore-types": "npm:3.0.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 651de02b5a07477d5a2911fee5dc82cc685adad82ab0f02bcf9c3a121dd929624d4cbcadb37134a470ecd80794dd282b604e3eddb57a17c884d54201a21d5edf + languageName: node + linkType: hard + +"@firebase/firestore-types@npm:3.0.1": + version: 3.0.1 + resolution: "@firebase/firestore-types@npm:3.0.1" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 4dbd0029d6196353b596ba7174abbbe1b183e6971a2ed95a9e2549801cfc3abe96f70d11def0bf3591f98be9d980802c4c2e9574c97b9c2365ba7b755362b786 + languageName: node + linkType: hard + +"@firebase/firestore@npm:4.6.0": + version: 4.6.0 + resolution: "@firebase/firestore@npm:4.6.0" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + "@firebase/webchannel-wrapper": "npm:0.10.6" + "@grpc/grpc-js": "npm:~1.9.0" + "@grpc/proto-loader": "npm:^0.7.8" + tslib: "npm:^2.1.0" + undici: "npm:5.28.4" + peerDependencies: + "@firebase/app": 0.x + checksum: 5b95f29070fc97430480e6f1532071ce4a8755adfd4bd12aa5b424ed5ca29341e80862cd11fc8f075c5cc889b7e6f5352c852652097e2f07d489fcdbc83045bf + languageName: node + linkType: hard + +"@firebase/functions-compat@npm:0.3.10": + version: 0.3.10 + resolution: "@firebase/functions-compat@npm:0.3.10" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/functions": "npm:0.11.4" + "@firebase/functions-types": "npm:0.6.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 3ade94e619a20be50f2dbff834ff3a79e0f58da31ee664596edfcbd41741afbfe162c15bb38795109d4776ed75346f2accad5f6b75dbd1a963dc75676c91fee3 + languageName: node + linkType: hard + +"@firebase/functions-types@npm:0.6.1": + version: 0.6.1 + resolution: "@firebase/functions-types@npm:0.6.1" + checksum: 90a9a9bcb6a73027a87d71d8cfc5007a0085593f094d90fc42be1086d87ee560d711ab7c03856985bb4a8742150f4e2eda3ffaf3bc8749c83d16217c17efe535 + languageName: node + linkType: hard + +"@firebase/functions@npm:0.11.4": + version: 0.11.4 + resolution: "@firebase/functions@npm:0.11.4" + dependencies: + "@firebase/app-check-interop-types": "npm:0.3.1" + "@firebase/auth-interop-types": "npm:0.2.2" + "@firebase/component": "npm:0.6.6" + "@firebase/messaging-interop-types": "npm:0.2.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + undici: "npm:5.28.4" + peerDependencies: + "@firebase/app": 0.x + checksum: 7cadc8bf72bef8828b4cd5c9b306a4f74420a3bc85ed8b8f43bb48977faeae0e56e4086b8c0f07277e2a18cd264e829d45f0d0b2f4520f66636396dcef93cbb9 + languageName: node + linkType: hard + +"@firebase/installations-compat@npm:0.2.6": + version: 0.2.6 + resolution: "@firebase/installations-compat@npm:0.2.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/installations": "npm:0.6.6" + "@firebase/installations-types": "npm:0.5.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 6ef7f881af268b89749b38e7f6a2d039b8ac39606559a47e80c6636063d377ffb71f8221552ec5cd112ded3b603ab49dcab6f9d98f82576bdc8fc2ae7e4be0a4 + languageName: node + linkType: hard + +"@firebase/installations-types@npm:0.5.1": + version: 0.5.1 + resolution: "@firebase/installations-types@npm:0.5.1" + peerDependencies: + "@firebase/app-types": 0.x + checksum: 786cf890f1ada25cfd6382506f310fe8a3f0a37d7d5c33fb5f0bf8beb2f4f942e4a1e36b4d1dd4f3106c5cd9d440ab382c984036d7b1edef73fc01e20eefa1d6 + languageName: node + linkType: hard + +"@firebase/installations@npm:0.6.6": + version: 0.6.6 + resolution: "@firebase/installations@npm:0.6.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/util": "npm:1.9.5" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 29349d9e7f9c1dd547557d1bebfcc6b10143b36ba47fb1b377f92fc5bfcf6936ba1afb6779b2aabf4a66a5335a4438c897f6d83a953aecc8f68520f2feb6520a + languageName: node + linkType: hard + +"@firebase/logger@npm:0.4.1": + version: 0.4.1 + resolution: "@firebase/logger@npm:0.4.1" + dependencies: + tslib: "npm:^2.1.0" + checksum: fc4af761deff0cdab7612d2124c162b5a5e38c2d9f123d14d4c9837cdd78537c9becbd889a00b753856ce69203824a9c0c4e62ad05c937c564a863c74db4b370 + languageName: node + linkType: hard + +"@firebase/messaging-compat@npm:0.2.8": + version: 0.2.8 + resolution: "@firebase/messaging-compat@npm:0.2.8" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/messaging": "npm:0.12.8" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: fd0376bc8c9249cb298f8a78c45f26716dd5e55581f49055fb8b7541f4216d531837621be680a3248423f7a2d2c8ef9ee2cec31a47356c012ca67e5410c9095a + languageName: node + linkType: hard + +"@firebase/messaging-interop-types@npm:0.2.1": + version: 0.2.1 + resolution: "@firebase/messaging-interop-types@npm:0.2.1" + checksum: 976767932b40159890b6b4564ff63a06420de69bfb4e2334105db15134a273b5a36770743369d1eb02f6c8201c75fa5fb4ec3de34fc433fc56e011a11eaafde4 + languageName: node + linkType: hard + +"@firebase/messaging@npm:0.12.8": + version: 0.12.8 + resolution: "@firebase/messaging@npm:0.12.8" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/installations": "npm:0.6.6" + "@firebase/messaging-interop-types": "npm:0.2.1" + "@firebase/util": "npm:1.9.5" + idb: "npm:7.1.1" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 0774b12d048afcbf190093c693693b3facdec7f70f034f1f662e7e42488f69a3984f45e867c05f61f0011091512457a98087981d32bb17b53941494e3718963a + languageName: node + linkType: hard + +"@firebase/performance-compat@npm:0.2.6": + version: 0.2.6 + resolution: "@firebase/performance-compat@npm:0.2.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/performance": "npm:0.6.6" + "@firebase/performance-types": "npm:0.2.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 0870b7c05dfcdff62dc712e4a500abed117154f359e3b43484c8207518517de1e8cb6546ba7035ef90e45bebb13f7d679a252a6ea3ec135b1b7f8cdc5068bf1b + languageName: node + linkType: hard + +"@firebase/performance-types@npm:0.2.1": + version: 0.2.1 + resolution: "@firebase/performance-types@npm:0.2.1" + checksum: c5186a33f2d1a5d581e57318a338099a8eb76fdd5de68c13965836338a8533178a97438ac259fa9d335b86f75f40c5b70a4387750eba85f986dcd86bf1ad2182 + languageName: node + linkType: hard + +"@firebase/performance@npm:0.6.6": + version: 0.6.6 + resolution: "@firebase/performance@npm:0.6.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/installations": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 331e12bdd34c416e5142037c4786c233288c7850ea8877d4ae1cec990cdd0b80f26f30ab40432a2358550722e66a52e26941bd5575419c76f204a97ce18298d3 + languageName: node + linkType: hard + +"@firebase/remote-config-compat@npm:0.2.6": + version: 0.2.6 + resolution: "@firebase/remote-config-compat@npm:0.2.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/remote-config": "npm:0.4.6" + "@firebase/remote-config-types": "npm:0.3.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 4593117743af6cbf22703d3e6736cfa1c63813ee64299ada3e4ae696c9ee3dc4ece5d6f153110ca5c58d0818755e790a730b6dd61744ac69b6cfda9036db1204 + languageName: node + linkType: hard + +"@firebase/remote-config-types@npm:0.3.1": + version: 0.3.1 + resolution: "@firebase/remote-config-types@npm:0.3.1" + checksum: bba5869f28b2abc80b2b498c1bf11c0aa1e69a5725868a2e698c3db3afaaebb15e90ff8aabe983771785a15c8d9567a8a3a751e8c7e41e609659fbec47b3b959 + languageName: node + linkType: hard + +"@firebase/remote-config@npm:0.4.6": + version: 0.4.6 + resolution: "@firebase/remote-config@npm:0.4.6" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/installations": "npm:0.6.6" + "@firebase/logger": "npm:0.4.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app": 0.x + checksum: 7a2a3bfd96c70b016bedf05a9ffde6ff8979a54f851086d115d1c3d8390a2e56085a344c0d270951d4adb55b71e1587c2cb890af9f612fda14b3337a2e7f88cd + languageName: node + linkType: hard + +"@firebase/storage-compat@npm:0.3.7": + version: 0.3.7 + resolution: "@firebase/storage-compat@npm:0.3.7" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/storage": "npm:0.12.4" + "@firebase/storage-types": "npm:0.8.1" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + peerDependencies: + "@firebase/app-compat": 0.x + checksum: 263234f8d3112d997c26c686ca2a8bfcd8c1fe4b817bd7af2cc83da5376000df79ebd93f1af82a73fdf7951e17eec9ab21decd02b9ee0c6029022595ba8e83cc + languageName: node + linkType: hard + +"@firebase/storage-types@npm:0.8.1": + version: 0.8.1 + resolution: "@firebase/storage-types@npm:0.8.1" + peerDependencies: + "@firebase/app-types": 0.x + "@firebase/util": 1.x + checksum: 84ccf99f048a1067f4f05d94915f2a3f9b2ba92984ef17a956268bcddcae878dfa8a8336726bb54472f8babb9d14eab776be5b45ef3e5e7f2a008e2565cb8be8 + languageName: node + linkType: hard + +"@firebase/storage@npm:0.12.4": + version: 0.12.4 + resolution: "@firebase/storage@npm:0.12.4" + dependencies: + "@firebase/component": "npm:0.6.6" + "@firebase/util": "npm:1.9.5" + tslib: "npm:^2.1.0" + undici: "npm:5.28.4" + peerDependencies: + "@firebase/app": 0.x + checksum: 0186ce9051cc5f8be3d92f15df64312baa0d9c13b50e20cbe5f54122cdcae547aae8d3061b69f0005e9df96af7785c41a912c6e376ad3b1ee723561a46705bf8 + languageName: node + linkType: hard + +"@firebase/util@npm:1.9.5": + version: 1.9.5 + resolution: "@firebase/util@npm:1.9.5" + dependencies: + tslib: "npm:^2.1.0" + checksum: 6827bdad90465bc53616db2fbb71f226106c976ec48c2fd82bf7b550f610e050734eba70aa6b49e6bd4cbdeb34972484d0740c2d17db137efae3e8cef235480d + languageName: node + linkType: hard + +"@firebase/webchannel-wrapper@npm:0.10.6": + version: 0.10.6 + resolution: "@firebase/webchannel-wrapper@npm:0.10.6" + checksum: ea09096fb197f7353141005cf34ffd249d02d03a8927cf535dc13293bbfe83b16dd6317c29383b7202a052b9ad59b66ff65e52a53d2314831e1bc7d0ddabfd94 + languageName: node + linkType: hard + "@fivebinaries/coin-selection@npm:2.2.1": version: 2.2.1 resolution: "@fivebinaries/coin-selection@npm:2.2.1" @@ -3016,6 +3544,30 @@ __metadata: languageName: node linkType: hard +"@grpc/grpc-js@npm:~1.9.0": + version: 1.9.14 + resolution: "@grpc/grpc-js@npm:1.9.14" + dependencies: + "@grpc/proto-loader": "npm:^0.7.8" + "@types/node": "npm:>=12.12.47" + checksum: 417f8ce1b0a529b05f18f1432ccbe257ad4b305ad04b548dcc502adcffde48dfaa4f392e71cb782bfebc1fa23c1f32baed83da368d8c05296da0a243020a60d2 + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.7.8": + version: 0.7.12 + resolution: "@grpc/proto-loader@npm:0.7.12" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.2.4" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: c8a9f915d44881ca7dce108dfb81d853912d95d756308f1ea6b688f63c5342ada4fe0a7cfacc0b28f89a77a4e65cce91fad99e65d5ae49b3d4e1ec4863f84ad4 + languageName: node + linkType: hard + "@gulp-sourcemaps/identity-map@npm:^2.0.1": version: 2.0.1 resolution: "@gulp-sourcemaps/identity-map@npm:2.0.1" @@ -10029,12 +10581,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17": - version: 20.12.3 - resolution: "@types/node@npm:20.12.3" +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17": + version: 20.12.7 + resolution: "@types/node@npm:20.12.7" dependencies: undici-types: "npm:~5.26.4" - checksum: 3f3c5c6ba118a18aa997c51cdc3c66259d69021f87475a99ed6c913a6956e22de49748e09843bf6447a7a63ae474e61945a6dbcca93e23b2359fc0b6f9914f7a + checksum: b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801 languageName: node linkType: hard @@ -18286,6 +18838,15 @@ __metadata: languageName: node linkType: hard +"faye-websocket@npm:0.11.4": + version: 0.11.4 + resolution: "faye-websocket@npm:0.11.4" + dependencies: + websocket-driver: "npm:>=0.5.1" + checksum: 22433c14c60925e424332d2794463a8da1c04848539b5f8db5fced62a7a7c71a25335a4a8b37334e3a32318835e2b87b1733d008561964121c4a0bd55f0878c3 + languageName: node + linkType: hard + "faye-websocket@npm:~0.10.0": version: 0.10.0 resolution: "faye-websocket@npm:0.10.0" @@ -18631,6 +19192,40 @@ __metadata: languageName: node linkType: hard +"firebase@npm:^10.11.0": + version: 10.11.0 + resolution: "firebase@npm:10.11.0" + dependencies: + "@firebase/analytics": "npm:0.10.2" + "@firebase/analytics-compat": "npm:0.2.8" + "@firebase/app": "npm:0.10.1" + "@firebase/app-check": "npm:0.8.3" + "@firebase/app-check-compat": "npm:0.3.10" + "@firebase/app-compat": "npm:0.2.31" + "@firebase/app-types": "npm:0.9.1" + "@firebase/auth": "npm:1.7.1" + "@firebase/auth-compat": "npm:0.5.6" + "@firebase/database": "npm:1.0.4" + "@firebase/database-compat": "npm:1.0.4" + "@firebase/firestore": "npm:4.6.0" + "@firebase/firestore-compat": "npm:0.3.29" + "@firebase/functions": "npm:0.11.4" + "@firebase/functions-compat": "npm:0.3.10" + "@firebase/installations": "npm:0.6.6" + "@firebase/installations-compat": "npm:0.2.6" + "@firebase/messaging": "npm:0.12.8" + "@firebase/messaging-compat": "npm:0.2.8" + "@firebase/performance": "npm:0.6.6" + "@firebase/performance-compat": "npm:0.2.6" + "@firebase/remote-config": "npm:0.4.6" + "@firebase/remote-config-compat": "npm:0.2.6" + "@firebase/storage": "npm:0.12.4" + "@firebase/storage-compat": "npm:0.3.7" + "@firebase/util": "npm:1.9.5" + checksum: b57ccbceaa528889cfaeb105f037fe3b5a91e3f7586f94cd1018197715d4daff6b1daf0dd3c070c5446c343f22960dacd0a2207b026b2a938f3affc3bba84c8e + languageName: node + linkType: hard + "first-chunk-stream@npm:3.0.0, first-chunk-stream@npm:^3.0.0": version: 3.0.0 resolution: "first-chunk-stream@npm:3.0.0" @@ -20767,6 +21362,13 @@ __metadata: languageName: node linkType: hard +"idb@npm:7.1.1": + version: 7.1.1 + resolution: "idb@npm:7.1.1" + checksum: 8e33eaebf21055129864acb89932e0739b8c96788e559df24c253ce114d8c6deb977a3b30ea47a9bb8a2ae8a55964861c3df65f360d95745e341cee40d5c17f4 + languageName: node + linkType: hard + "idna-uts46-hx@npm:^2.3.1": version: 2.3.1 resolution: "idna-uts46-hx@npm:2.3.1" @@ -25070,6 +25672,7 @@ __metadata: fancy-log: "npm:^1.3.3" fast-glob: "npm:^3.2.2" fast-json-patch: "npm:^3.1.1" + firebase: "npm:^10.11.0" fs-extra: "npm:^8.1.0" fuse.js: "npm:^3.2.0" ganache: "patch:ganache@npm%3A7.9.2#~/.yarn/patches/ganache-npm-7.9.2-a70dc8da34.patch" @@ -28676,6 +29279,26 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.2.4": + version: 7.2.6 + resolution: "protobufjs@npm:7.2.6" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 81ab853d28c71998d056d6b34f83c4bc5be40cb0b416585f99ed618aed833d64b2cf89359bad7474d345302f2b5e236c4519165f8483d7ece7fd5b0d9ac13f8b + languageName: node + linkType: hard + "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -33979,6 +34602,15 @@ __metadata: languageName: node linkType: hard +"undici@npm:5.28.4": + version: 5.28.4 + resolution: "undici@npm:5.28.4" + dependencies: + "@fastify/busboy": "npm:^2.0.0" + checksum: a666a9f5ac4270c659fafc33d78b6b5039a0adbae3e28f934774c85dcc66ea91da907896f12b414bd6f578508b44d5dc206fa636afa0e49a4e1c9e99831ff065 + languageName: node + linkType: hard + "unherit@npm:^1.0.4": version: 1.1.2 resolution: "unherit@npm:1.1.2" From 0edd2daab2a8331a4de583558588c1c47364c5df Mon Sep 17 00:00:00 2001 From: Jony Bursztyn Date: Fri, 26 Apr 2024 13:29:16 +0100 Subject: [PATCH 005/107] fix: Remove snaps, add highlight to connection list (#24099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** On-hover behavior for Connected Site on All Permissions screen. Sites listed on the All Permissions screen should have an [on-hover bg color](https://metamask.github.io/metamask-storybook/?path=/docs/components-ui-listitem--docs) to indicate that they are clickable and will take users to a different screen. Snaps are removed from Permissions page [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24099?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/MetaMask/metamask-extension/assets/11148144/34b4599c-d96c-40be-85a3-c0983df40446 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: David Walsh --- app/_locales/en/messages.json | 9 - .../permissions-page.test.js.snap | 169 +----------------- .../permissions-page/connection-list-item.js | 1 + .../connection-list-item.scss | 7 + .../pages/permissions-page/index.scss | 2 + .../permissions-page/permissions-page.js | 77 +------- .../permissions-page/permissions-page.test.js | 46 ----- 7 files changed, 19 insertions(+), 292 deletions(-) create mode 100644 ui/components/multichain/pages/permissions-page/connection-list-item.scss create mode 100644 ui/components/multichain/pages/permissions-page/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7cbc09d4087e..1ddc4064d560 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4457,12 +4457,6 @@ "simulationsSettingSubHeader": { "message": "Estimate balance changes" }, - "siteConnections": { - "message": "Site Connections" - }, - "sites": { - "message": "Sites" - }, "skip": { "message": "Skip" }, @@ -4570,9 +4564,6 @@ "message": "$1 wants to use $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." }, - "snapConnections": { - "message": "Snap Connections" - }, "snapContent": { "message": "This content is coming from $1", "description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap." diff --git a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap index baf6e9450ad6..5f262171ce12 100644 --- a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap +++ b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap @@ -41,19 +41,13 @@ exports[`All Connections render renders correctly 1`] = `
-

- Site Connections -

-

- Snap Connections -

- - -
diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.js b/ui/components/multichain/pages/permissions-page/connection-list-item.js index 666c233ba88b..8d06e7b2b59c 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.js @@ -46,6 +46,7 @@ export const ConnectionListItem = ({ connection, onClick }) => { onClick={onClick} padding={4} gap={4} + className="multichain-connection-list-item" > { const t = useI18nContext(); const history = useHistory(); @@ -47,24 +43,12 @@ export const PermissionsPage = () => { const sitesConnectionsList = useSelector( getConnectedSitesListWithNetworkInfo, ); - const snapsConnectionsList = useSelector(getConnectedSnapsList); const showPermissionsTour = useSelector(getShowPermissionsTour); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); useEffect(() => { - setTotalConnections( - Object.keys(sitesConnectionsList).length + - Object.keys(snapsConnectionsList).length, - ); - }, [sitesConnectionsList, snapsConnectionsList]); - - const shouldShowTabsView = useMemo(() => { - return ( - totalConnections > TABS_THRESHOLD && - Object.keys(sitesConnectionsList).length > 0 && - Object.keys(snapsConnectionsList).length > 0 - ); - }, [totalConnections, sitesConnectionsList, snapsConnectionsList]); + setTotalConnections(Object.keys(sitesConnectionsList).length); + }, [sitesConnectionsList]); const handleConnectionClick = (connection) => { const hostName = connection.origin; @@ -121,58 +105,11 @@ export const PermissionsPage = () => { positionObj="44%" /> ) : null} - + - {shouldShowTabsView ? ( - - - {renderConnectionsList(sitesConnectionsList)} - - - {renderConnectionsList(snapsConnectionsList)} - - + {totalConnections > 0 ? ( + renderConnectionsList(sitesConnectionsList) ) : ( - <> - {Object.keys(sitesConnectionsList).length > 0 && ( - <> - - {t('siteConnections')} - - {renderConnectionsList(sitesConnectionsList)} - - )} - {Object.keys(snapsConnectionsList).length > 0 && ( - <> - - {t('snapConnections')} - - {renderConnectionsList(snapsConnectionsList)} - - )} - - )} - {totalConnections === 0 ? ( { {t('permissionsPageEmptySubContent')} - ) : null} + )} ); diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.test.js b/ui/components/multichain/pages/permissions-page/permissions-page.test.js index 1440f8c389c8..dacdda2f25d3 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.test.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.test.js @@ -100,52 +100,6 @@ describe('All Connections', () => { expect(getByTestId('permissions-page')).toBeInTheDocument(); }); - it('renders sections when user has 5 or less connections', () => { - const { getByTestId } = renderWithProvider(, store); - expect(getByTestId('sites-connections')).toBeInTheDocument(); - expect(getByTestId('snaps-connections')).toBeInTheDocument(); - }); - - it('renders tabs when user has more than 5 connections', () => { - mockState.metamask.snaps = { - ...mockState.metamask.snaps, - 'npm:@metamask/testSnap4': { - id: 'npm:@metamask/testSnap4', - origin: 'npm:@metamask/testSnap4', - version: '5.1.2', - iconUrl: null, - initialPermissions: { - 'endowment:ethereum-provider': {}, - }, - }, - 'npm:@metamask/testSnap5': { - id: 'npm:@metamask/testSnap5', - origin: 'npm:@metamask/testSnap5', - version: '5.1.2', - iconUrl: null, - initialPermissions: { - 'endowment:ethereum-provider': {}, - }, - }, - }; - mockState.metamask.subjectMetadata = { - ...mockState.metamask.subjectMetadata, - 'npm:@metamask/testSnap4': { - name: 'Test Snap 4', - version: '1.2.3', - subjectType: 'snap', - }, - 'npm:@metamask/testSnap5': { - name: 'Test Snap 5', - version: '1.2.3', - subjectType: 'snap', - }, - }; - store = configureStore(mockState); - const { getByTestId } = renderWithProvider(, store); - expect(getByTestId('permissions-page-sites-tab')).toBeInTheDocument(); - expect(getByTestId('permissions-page-snaps-tab')).toBeInTheDocument(); - }); it('renders no connections message when user has no connections', () => { mockState.metamask.snaps = {}; mockState.metamask.subjectMetadata = {}; From a07d82f5c2c3b3bdc42d9f267323758e92be928b Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 26 Apr 2024 18:20:43 +0530 Subject: [PATCH 006/107] Fix confirm page in extended view (#24211) --- .../confirm/__snapshots__/confirm.test.tsx.snap | 4 ++-- ui/pages/confirmations/confirm/confirm.tsx | 2 +- ui/pages/confirmations/confirm/index.scss | 9 +++++++++ ui/pages/confirmations/index.scss | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 ui/pages/confirmations/confirm/index.scss diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index cfce9679d103..c6d0e10dc1f5 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Confirm matches snapshot for personal signature type 1`] = ` class="mm-box multichain-page mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--width-full mm-box--height-full mm-box--background-color-background-alternative" >
{ syncConfirmPath(); return ( - +