From b5472e1b05b8efdca1a631212fb311d3bd93e6b8 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 17:31:10 +0000 Subject: [PATCH 01/10] feat: cp-7.60.0 gas station support for metamask pay deposits (#23033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Support EIP-7702 gas station when depositing with MetaMask Pay. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [#6150](https://github.com/MetaMask/MetaMask-planning/issues/6150) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.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. --- > [!NOTE] > Adds gas fee token awareness to insufficient balance alerts, simplifies available token selection logic, delegates getGasFeeTokens, and upgrades transaction-pay-controller to v10. > > - **Confirmations / Alerts**: > - Consider `fees.isSourceGasFeeToken` in fee/amount calculations and source network checks within `useInsufficientPayTokenBalanceAlert`. > - Add tests covering gas fee token scenarios and adjust mocks accordingly. > - **Utils (`transaction-pay`)**: > - Simplify `getAvailableTokens`: remove "no native gas" disablement and related i18n; return tokens based on balance/selection/required status only. > - Update tests to drop disabled-message case. > - **Engine / Messenger**: > - Delegate `TransactionController:getGasFeeTokens` to the Transaction Pay controller messenger. > - **Dependencies**: > - Bump `@metamask/transaction-pay-controller` to `^10.0.0` (lockfile updated). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a0d198d0d87eb68a04f886f4bf3aab2f1531a0a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- ...seInsufficientPayTokenBalanceAlert.test.ts | 78 +++++++++++++++++-- .../useInsufficientPayTokenBalanceAlert.ts | 11 ++- .../utils/transaction-pay.test.ts | 20 ----- .../confirmations/utils/transaction-pay.ts | 16 ---- .../transaction-pay-controller-messenger.ts | 1 + package.json | 2 +- yarn.lock | 16 ++-- 7 files changed, 87 insertions(+), 57 deletions(-) diff --git a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts index 1a3805a05606..1182a79a70b0 100644 --- a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts +++ b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.test.ts @@ -87,7 +87,7 @@ describe('useInsufficientPayTokenBalanceAlert', () => { }); it('returns alert if pay token balance is less than required token amount', () => { - useTransactionPayTokenMock.mockReturnValueOnce({ + useTransactionPayTokenMock.mockReturnValue({ payToken: { ...PAY_TOKEN_MOCK, balanceUsd: '1.22', @@ -111,14 +111,14 @@ describe('useInsufficientPayTokenBalanceAlert', () => { }); it('ignores required token amount if skipIfBalance', () => { - useTransactionPayRequiredTokensMock.mockReturnValueOnce([ + useTransactionPayRequiredTokensMock.mockReturnValue([ { ...REQUIRED_TOKEN_MOCK, skipIfBalance: true, }, ]); - useTransactionPayTokenMock.mockReturnValueOnce({ + useTransactionPayTokenMock.mockReturnValue({ payToken: { ...PAY_TOKEN_MOCK, balanceUsd: '1.22', @@ -134,7 +134,7 @@ describe('useInsufficientPayTokenBalanceAlert', () => { describe('for fees', () => { it('returns alert if pay token balance is less than total source amount', () => { - useTransactionPayTokenMock.mockReturnValueOnce({ + useTransactionPayTokenMock.mockReturnValue({ payToken: { ...PAY_TOKEN_MOCK, balanceRaw: '999', @@ -160,7 +160,7 @@ describe('useInsufficientPayTokenBalanceAlert', () => { }); it('returns alert if pay token balance is less than source amount plus source network', () => { - useTransactionPayTokenMock.mockReturnValueOnce({ + useTransactionPayTokenMock.mockReturnValue({ payToken: { ...PAY_TOKEN_MOCK, address: NATIVE_TOKEN_MOCK.address as Hex, @@ -185,11 +185,45 @@ describe('useInsufficientPayTokenBalanceAlert', () => { }, ]); }); + + it('returns alert if pay token balance is less than source amount plus source network if gas fee token', () => { + useTransactionPayTokenMock.mockReturnValue({ + payToken: { + ...PAY_TOKEN_MOCK, + balanceRaw: '1099', + }, + setPayToken: jest.fn(), + }); + + useTransactionPayTotalsMock.mockReturnValue({ + ...TOTALS_MOCK, + fees: { + ...TOTALS_MOCK.fees, + isSourceGasFeeToken: true, + }, + }); + + const { result } = runHook(); + + expect(result.current).toStrictEqual([ + { + key: AlertKeys.InsufficientPayTokenFees, + field: RowAlertKey.Amount, + isBlocking: true, + title: strings('alert_system.insufficient_pay_token_balance.message'), + message: strings( + 'alert_system.insufficient_pay_token_balance_fees.message', + { amount: '$1.11' }, + ), + severity: Severity.Danger, + }, + ]); + }); }); describe('for source network fee', () => { it('returns alert if native balance is less than total source network fee', () => { - useTokenWithBalanceMock.mockReturnValueOnce({ + useTokenWithBalanceMock.mockReturnValue({ ...NATIVE_TOKEN_MOCK, balanceRaw: '99', } as ReturnType); @@ -212,12 +246,12 @@ describe('useInsufficientPayTokenBalanceAlert', () => { }); it('returns no alert if pay token is native', () => { - useTokenWithBalanceMock.mockReturnValueOnce({ + useTokenWithBalanceMock.mockReturnValue({ ...NATIVE_TOKEN_MOCK, balanceRaw: '99', } as ReturnType); - useTransactionPayTokenMock.mockReturnValueOnce({ + useTransactionPayTokenMock.mockReturnValue({ payToken: { ...PAY_TOKEN_MOCK, address: NATIVE_TOKEN_MOCK.address as Hex, @@ -230,5 +264,33 @@ describe('useInsufficientPayTokenBalanceAlert', () => { expect(result.current).toStrictEqual([]); }); + + it('returns no alert if source network is using gas fee token', () => { + useTokenWithBalanceMock.mockReturnValue({ + ...NATIVE_TOKEN_MOCK, + balanceRaw: '99', + } as ReturnType); + + useTransactionPayTotalsMock.mockReturnValue({ + ...TOTALS_MOCK, + fees: { + ...TOTALS_MOCK.fees, + isSourceGasFeeToken: true, + }, + sourceAmount: TOTALS_MOCK.sourceAmount, + }); + + useTransactionPayTokenMock.mockReturnValue({ + payToken: { + ...PAY_TOKEN_MOCK, + balanceRaw: '1100', + }, + setPayToken: jest.fn(), + }); + + const { result } = runHook(); + + expect(result.current).toStrictEqual([]); + }); }); }); diff --git a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts index e84799dc2e1b..3d5ec92493fa 100644 --- a/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts +++ b/app/components/Views/confirmations/hooks/alerts/useInsufficientPayTokenBalanceAlert.ts @@ -27,6 +27,7 @@ export function useInsufficientPayTokenBalanceAlert({ const totals = useTransactionPayTotals(); const formatFiat = useFiatFormatter({ currency: 'usd' }); const isLoading = useIsTransactionPayLoading(); + const isSourceGasFeeToken = totals?.fees.isSourceGasFeeToken ?? false; const sourceChainId = payToken?.chainId ?? '0x0'; @@ -64,20 +65,20 @@ export function useInsufficientPayTokenBalanceAlert({ } return new BigNumber(totals?.sourceAmount.raw ?? '0').plus( - isPayTokenNative + isPayTokenNative || isSourceGasFeeToken ? new BigNumber(totals?.fees.sourceNetwork.max.raw ?? '0') : '0', ); - }, [isLoading, isPayTokenNative, totals]); + }, [isLoading, isPayTokenNative, isSourceGasFeeToken, totals]); const totalSourceAmountUsd = useMemo( () => new BigNumber(totals?.sourceAmount.usd ?? '0').plus( - isPayTokenNative + isPayTokenNative || isSourceGasFeeToken ? new BigNumber(totals?.fees.sourceNetwork.max.usd ?? '0') : '0', ), - [isPayTokenNative, totals], + [isPayTokenNative, isSourceGasFeeToken, totals], ); const targetAmountUsd = useMemo(() => { @@ -104,9 +105,11 @@ export function useInsufficientPayTokenBalanceAlert({ () => payToken && !isPayTokenNative && + !isSourceGasFeeToken && totalSourceNetworkFeeRaw.isGreaterThan(nativeToken?.balanceRaw ?? '0'), [ isPayTokenNative, + isSourceGasFeeToken, nativeToken?.balanceRaw, payToken, totalSourceNetworkFeeRaw, diff --git a/app/components/Views/confirmations/utils/transaction-pay.test.ts b/app/components/Views/confirmations/utils/transaction-pay.test.ts index 63d96df0f707..0585b81bc0ad 100644 --- a/app/components/Views/confirmations/utils/transaction-pay.test.ts +++ b/app/components/Views/confirmations/utils/transaction-pay.test.ts @@ -19,7 +19,6 @@ import { TransactionPaymentToken, } from '@metamask/transaction-pay-controller'; import { Hex } from '@metamask/utils'; -import { strings } from '../../../../../locales/i18n'; const CHAIN_ID_MOCK = '0x1'; const TO_MOCK = '0x0987654321098765432109876543210987654321'; @@ -200,25 +199,6 @@ describe('Transaction Pay Utils', () => { expect(result).toMatchObject([tokenWithZeroBalance]); }); - it('returns disabled token with message if no native gas', async () => { - const nonNativeToken = { - ...TOKEN_MOCK, - address: '0x234', - } as AssetType; - - const result = getAvailableTokens({ - tokens: [nonNativeToken] as AssetType[], - }); - - expect(result).toMatchObject([ - { - ...nonNativeToken, - disabled: true, - disabledMessage: strings('pay_with_modal.no_gas'), - }, - ]); - }); - it('does not return token if no balance', async () => { const tokenWithZeroBalance = { ...TOKEN_MOCK, diff --git a/app/components/Views/confirmations/utils/transaction-pay.ts b/app/components/Views/confirmations/utils/transaction-pay.ts index f590eff7c9ba..1287d9726bc4 100644 --- a/app/components/Views/confirmations/utils/transaction-pay.ts +++ b/app/components/Views/confirmations/utils/transaction-pay.ts @@ -11,8 +11,6 @@ import { TransactionPayRequiredToken, TransactionPaymentToken, } from '@metamask/transaction-pay-controller'; -import { getNativeTokenAddress } from './asset'; -import { strings } from '../../../../../locales/i18n'; import { BigNumber } from 'bignumber.js'; import { isTestNet } from '../../../../util/networks'; @@ -126,22 +124,8 @@ export function getAvailableTokens({ payToken?.address.toLowerCase() === token.address.toLowerCase() && payToken?.chainId === token.chainId; - const nativeTokenAddress = getNativeTokenAddress(token.chainId as Hex); - - const nativeToken = tokens.find( - (t) => t.address === nativeTokenAddress && t.chainId === token.chainId, - ); - - const disabled = new BigNumber(nativeToken?.balance ?? 0).isZero(); - - const disabledMessage = disabled - ? strings('pay_with_modal.no_gas') - : undefined; - return { ...token, - disabled, - disabledMessage, isSelected, }; }); diff --git a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts index 90064564ec6a..65112dbe6cd0 100644 --- a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts +++ b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts @@ -35,6 +35,7 @@ export function getTransactionPayControllerMessenger( 'TokenListController:getState', 'TokenRatesController:getState', 'TokensController:getState', + 'TransactionController:getGasFeeTokens', 'TransactionController:getState', 'TransactionController:updateTransaction', ], diff --git a/package.json b/package.json index 035e8f47b45c..6512e9730701 100644 --- a/package.json +++ b/package.json @@ -286,7 +286,7 @@ "@metamask/swaps-controller": "^15.0.0", "@metamask/token-search-discovery-controller": "^4.0.0", "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch", - "@metamask/transaction-pay-controller": "^9.0.0", + "@metamask/transaction-pay-controller": "^10.0.0", "@metamask/tron-wallet-snap": "^1.8.0", "@metamask/utils": "^11.8.1", "@ngraveio/bc-ur": "^1.1.6", diff --git a/yarn.lock b/yarn.lock index 80c29248cee5..bb0a27fba3d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8965,9 +8965,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-pay-controller@npm:^9.0.0": - version: 9.0.0 - resolution: "@metamask/transaction-pay-controller@npm:9.0.0" +"@metamask/transaction-pay-controller@npm:^10.0.0": + version: 10.0.0 + resolution: "@metamask/transaction-pay-controller@npm:10.0.0" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" @@ -8981,14 +8981,14 @@ __metadata: immer: "npm:^9.0.6" lodash: "npm:^4.17.21" peerDependencies: - "@metamask/assets-controllers": ^90.0.0 - "@metamask/bridge-controller": ^62.0.0 - "@metamask/bridge-status-controller": ^62.0.0 + "@metamask/assets-controllers": ^91.0.0 + "@metamask/bridge-controller": ^63.0.0 + "@metamask/bridge-status-controller": ^63.0.0 "@metamask/gas-fee-controller": ^26.0.0 "@metamask/network-controller": ^26.0.0 "@metamask/remote-feature-flag-controller": ^2.0.0 "@metamask/transaction-controller": ^62.0.0 - checksum: 10/f97b313e75b4229d4cf0213449e4505164a3e4cb276e4b01f04347341dcf01fa3e89f21ea5df2def1fd608acddf574c11b517f4d364bf4f82ad59e11922cf2e3 + checksum: 10/596b50c04ee658bd16aefc8000d8cdbe2ac04e82636e9029b828c352377ca1e1af6b3c33f129ea28ba966553f513f42988e6ad50bc4edb99c59a68467fe6a1f6 languageName: node linkType: hard @@ -34467,7 +34467,7 @@ __metadata: "@metamask/test-dapp-solana": "npm:^0.3.0" "@metamask/token-search-discovery-controller": "npm:^4.0.0" "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" - "@metamask/transaction-pay-controller": "npm:^9.0.0" + "@metamask/transaction-pay-controller": "npm:^10.0.0" "@metamask/tron-wallet-snap": "npm:^1.8.0" "@metamask/utils": "npm:^11.8.1" "@ngraveio/bc-ur": "npm:^1.1.6" From 966ea794b10ea619dda03e88413321b589a5904f Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Thu, 20 Nov 2025 18:53:47 +0000 Subject: [PATCH 02/10] feat: refactor and fix invalid RPC URL error on paste (#22875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This commit fixes a bug on Android where pasting a valid RPC URL into the network settings would incorrectly trigger an "Invalid RPC URL" error. The bug was caused by the validation logic being tied to the `onBlur` event, which was not firing correctly after a paste. The fix moves the validation to the `onChangeText` event, ensuring that it is triggered immediately after the text is pasted. As part of this fix, the RPC URL input field and its validation logic have been refactored into a new, isolated component called `RpcUrlInput`. This improves the testability of the component and the overall code quality. A new test suite has been added for the `RpcUrlInput` component, and the old, now-obsolete tests have been removed. The new files have been created in TypeScript, and PropTypes have been replaced with TypeScript types. The form has also been cleaned up to remove old feature flags which led to very complex conditional logic. ## **Changelog** CHANGELOG entry: fix: invalid RPC URL error on paste ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/17161 https://consensyssoftware.atlassian.net/browse/ASSETS-1176 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** https://www.loom.com/share/c415ef5d1d6b497ea9775ece3b041608 https://www.loom.com/share/78d8c8c6b10542389ddccdf748c2b0b8 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.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. --- > [!NOTE] > Fixes invalid RPC URL error on paste by validating onChange via new RpcUrlInput component and simplifies NetworkSettings by removing legacy UI flag paths. > > - **Networks Settings UI** > - **New `RpcUrlInput` component**: Extracts RPC URL field with inline validation on `onChangeText`, warning display, and success callback. > - **Integration**: Uses `RpcUrlInput` in the Add RPC modal; updates state via `onValidationChange` and triggers `validateRpcAndChainId` on success. > - **Validation changes**: RPC URL validation no longer tied to `onBlur`; chain ID validation paths simplified; removed `isNetworkUiRedesignEnabled` conditionals across rendering and logic. > - **UI cleanup**: Consolidates buttons, warning banners, and dropdown usage for RPC and block explorer selectors. > - **Tests** > - Adds unit tests for `RpcUrlInput` (valid paste, invalid/duplicate/existing cases). > - Updates `NetworkSettings` tests and snapshots; removes obsolete tests tied to the old validation flow and feature flag. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit af5e9bab98d5e58723507eb9139d85b9a84ba805. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../NetworkSettings/RpcUrlInput.test.tsx | 92 ++++ .../NetworkSettings/RpcUrlInput.tsx | 122 ++++++ .../__snapshots__/index.test.tsx.snap | 33 +- .../NetworksSettings/NetworkSettings/index.js | 404 +++++------------- .../NetworkSettings/index.test.tsx | 149 +------ 5 files changed, 331 insertions(+), 469 deletions(-) create mode 100644 app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.test.tsx create mode 100644 app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.tsx diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.test.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.test.tsx new file mode 100644 index 000000000000..a0c1fbc039d9 --- /dev/null +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, userEvent, waitFor } from '@testing-library/react-native'; +import RpcUrlInput from './RpcUrlInput'; +import { strings } from '../../../../../../locales/i18n'; + +describe('RpcUrlInput', () => { + const mockCheckIfNetworkExists = jest.fn(); + const mockCheckIfRpcUrlExists = jest.fn(); + const mockOnValidationChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not show an error when a valid RPC URL is pasted', async () => { + mockCheckIfNetworkExists.mockResolvedValue([]); + mockCheckIfRpcUrlExists.mockResolvedValue([]); + + const { getByTestId, queryByText } = render( + , + ); + + const input = getByTestId('rpc-url-input'); + const validRpcUrl = 'https://mainnet.infura.io/v3/123'; + + // Simulate user pasting the URL + await userEvent.paste(input, validRpcUrl); + + // Check that the error message is not present + await waitFor(() => + expect( + queryByText(strings('app_settings.invalid_rpc_url')), + ).not.toBeOnTheScreen(), + ); + }); + + it.each([ + { + description: 'is invalid', + rpcUrl: 'invalid-url', + errorMessage: strings('app_settings.invalid_rpc_prefix'), + existingNetworks: [], + existingRPCs: [], + }, + { + description: 'already exists', + rpcUrl: 'https://mainnet.infura.io/v3/123', + errorMessage: strings('app_settings.url_associated_to_another_chain_id'), + existingNetworks: [{ network: 'mainnet' }], + existingRPCs: [], + }, + { + description: 'is a duplicate', + rpcUrl: 'https://mainnet.infura.io/v3/123', + errorMessage: strings('app_settings.invalid_rpc_url'), + existingNetworks: [], + existingRPCs: [{ network: 'mainnet' }], + }, + ])( + 'shows an error when the RPC URL $description', + async ({ rpcUrl, errorMessage, existingNetworks, existingRPCs }) => { + mockCheckIfNetworkExists.mockResolvedValue(existingNetworks); + mockCheckIfRpcUrlExists.mockResolvedValue(existingRPCs); + + const { getByTestId, findByText } = render( + , + ); + + const input = getByTestId('rpc-url-input'); + + // Simulate user pasting the URL + await userEvent.paste(input, rpcUrl); + + // Check that the error message is present + expect(await findByText(errorMessage)).toBeOnTheScreen(); + }, + ); +}); diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.tsx new file mode 100644 index 000000000000..f1d4d30ecfe7 --- /dev/null +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/RpcUrlInput.tsx @@ -0,0 +1,122 @@ +import React, { useState, useCallback, RefObject } from 'react'; +import { View, TextInput, TextInputProps, TextStyle } from 'react-native'; +import URLParse from 'url-parse'; +import { isWebUri } from 'valid-url'; +import { strings } from '../../../../../../locales/i18n'; +import { isPrivateConnection } from '../../../../../util/networks'; +import Text from '../../../../../component-library/components/Texts/Text'; +import { NetworksViewSelectorsIDs } from '../../../../../../e2e/selectors/Settings/NetworksView.selectors'; + +type InputProps = Pick< + TextInputProps, + | 'autoCapitalize' + | 'autoCorrect' + | 'editable' + | 'keyboardAppearance' + | 'onBlur' + | 'onFocus' + | 'onChangeText' + | 'onSubmitEditing' + | 'placeholder' + | 'placeholderTextColor' + | 'style' + | 'testID' + | 'value' +> & { + ref?: RefObject; +}; + +interface RpcUrlInputProps extends InputProps { + checkIfNetworkExists: (rpcUrl: string) => Promise; + checkIfRpcUrlExists: (rpcUrl: string) => Promise; + onValidationSuccess?: () => void; + onValidationChange: (isValid: boolean) => void; + warningStyle?: TextStyle; +} + +const RpcUrlInput: React.FC = (props) => { + const { + checkIfNetworkExists, + checkIfRpcUrlExists, + onValidationSuccess, + onValidationChange, + warningStyle, + ...inputProps + } = props; + const { onChangeText } = inputProps ?? {}; + + const [warningRpcUrl, setWarningRpcUrl] = useState( + undefined, + ); + + const validateRpcUrl = useCallback( + async (rpcUrl: string) => { + const isNetworkExists = await checkIfNetworkExists(rpcUrl); + const isRpcExists = await checkIfRpcUrlExists(rpcUrl); + if (!isWebUri(rpcUrl)) { + const appendedRpc = `http://${rpcUrl}`; + if (isWebUri(appendedRpc)) { + setWarningRpcUrl(strings('app_settings.invalid_rpc_prefix')); + } else { + setWarningRpcUrl(strings('app_settings.invalid_rpc_url')); + } + onValidationChange(false); + return false; + } + if (isRpcExists.length > 0) { + setWarningRpcUrl(strings('app_settings.invalid_rpc_url')); + onValidationChange(false); + return; + } + + if (isNetworkExists.length > 0) { + setWarningRpcUrl( + strings('app_settings.url_associated_to_another_chain_id'), + ); + onValidationChange(false); + return; + } + + const url = new URLParse(rpcUrl); + const privateConnection = isPrivateConnection(url.hostname); + if (!privateConnection && url.protocol === 'http:') { + setWarningRpcUrl(strings('app_settings.invalid_rpc_prefix')); + onValidationChange(false); + return false; + } + setWarningRpcUrl(undefined); + if (onValidationSuccess) { + onValidationSuccess(); + } + onValidationChange(true); + return true; + }, + [ + checkIfNetworkExists, + checkIfRpcUrlExists, + onValidationChange, + onValidationSuccess, + ], + ); + + const handleRpcUrlChange = useCallback( + (url: string) => { + onChangeText?.(url); + validateRpcUrl(url); + }, + [onChangeText, validateRpcUrl], + ); + + return ( + <> + + {warningRpcUrl && ( + + {warningRpcUrl} + + )} + + ); +}; + +export default RpcUrlInput; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap index ba068aa9447c..66324fe44313 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap @@ -31,38 +31,7 @@ exports[`NetworkSettings should render correctly 1`] = ` `; -exports[`NetworkSettings should render the component correctly when isNetworkUiRedesignEnabled is false 1`] = ` - - - -`; - -exports[`NetworkSettings should render the component correctly when isNetworkUiRedesignEnabled is true 1`] = ` +exports[`NetworkSettings should render the component correctly 1`] = ` { @@ -510,13 +503,6 @@ export class NetworkSettings extends PureComponent { ).filter((item) => item.rpcUrl === rpcUrl); if (checkCustomNetworks.length > 0) { - if (!isNetworkUiRedesignEnabled()) { - this.setState({ - warningRpcUrl: strings('app_settings.network_exists'), - }); - return checkCustomNetworks; - } - return checkCustomNetworks; } const defaultNetworks = getAllNetworks().map((item) => Networks[item]); @@ -637,14 +623,9 @@ export class NetworkSettings extends PureComponent { } // Conditionally check existence of network (Only check in Add Mode) - let isNetworkExists; - if (isNetworkUiRedesignEnabled()) { - isNetworkExists = addMode - ? await this.checkIfNetworkNotExistsByChainId(stateChainId) - : []; - } else { - isNetworkExists = editable ? [] : await this.checkIfNetworkExists(rpcUrl); - } + const isNetworkExists = addMode + ? await this.checkIfNetworkNotExistsByChainId(stateChainId) + : []; const isOnboarded = getIsNetworkOnboarded( stateChainId, @@ -710,58 +691,6 @@ export class NetworkSettings extends PureComponent { * Validates rpc url, setting a warningRpcUrl if is invalid * It also changes validatedRpcURL to true, indicating that was validated */ - validateRpcUrl = async (rpcUrl) => { - const isNetworkExists = await this.checkIfNetworkExists(rpcUrl); - const isRpcExists = await this.checkIfRpcUrlExists(rpcUrl); - - if (!isWebUri(rpcUrl)) { - const appendedRpc = `http://${rpcUrl}`; - if (isWebUri(appendedRpc)) { - this.setState({ - warningRpcUrl: strings('app_settings.invalid_rpc_prefix'), - }); - } else { - this.setState({ - warningRpcUrl: strings('app_settings.invalid_rpc_url'), - }); - } - return false; - } - - if (isRpcExists.length > 0) { - return this.setState({ - warningRpcUrl: strings('app_settings.invalid_rpc_url'), - }); - } - - if (isNetworkExists.length > 0) { - if (isNetworkUiRedesignEnabled()) { - return this.setState({ - validatedRpcURL: false, - warningRpcUrl: strings( - 'app_settings.url_associated_to_another_chain_id', - ), - }); - } - return this.setState({ - validatedRpcURL: true, - warningRpcUrl: strings('app_settings.network_exists'), - }); - } - const url = new URL(rpcUrl); - const privateConnection = isPrivateConnection(url.hostname); - if (!privateConnection && url.protocol === 'http:') { - this.setState({ - warningRpcUrl: strings('app_settings.invalid_rpc_prefix'), - }); - return false; - } - this.setState({ validatedRpcURL: true, warningRpcUrl: undefined }); - - this.validateRpcAndChainId(); - - return true; - }; /** * Validates that chain id is a valid integer number, setting a warningChainId if is invalid @@ -771,12 +700,7 @@ export class NetworkSettings extends PureComponent { const isChainIdExists = await this.checkIfChainIdExists(chainId); const isNetworkExists = await this.checkIfNetworkExists(rpcUrl); - if ( - isChainIdExists && - isNetworkExists.length > 0 && - isNetworkUiRedesignEnabled() && - !editable - ) { + if (isChainIdExists && isNetworkExists.length > 0 && !editable) { return this.setState({ validateChainId: true, warningChainId: strings( @@ -785,12 +709,7 @@ export class NetworkSettings extends PureComponent { }); } - if ( - isChainIdExists && - isNetworkExists.length === 0 && - isNetworkUiRedesignEnabled() && - !editable - ) { + if (isChainIdExists && isNetworkExists.length === 0 && !editable) { return this.setState({ validateChainId: true, warningChainId: strings('app_settings.network_already_exist'), @@ -848,10 +767,7 @@ export class NetworkSettings extends PureComponent { providerError = err; } - if ( - (providerError || typeof endpointChainId !== 'string') && - isNetworkUiRedesignEnabled() - ) { + if (providerError || typeof endpointChainId !== 'string') { return this.setState({ validatedRpcURL: false, warningRpcUrl: strings('app_settings.unMatched_chain'), @@ -859,15 +775,13 @@ export class NetworkSettings extends PureComponent { } if (endpointChainId !== toHex(chainId)) { - if (isNetworkUiRedesignEnabled()) { - return this.setState({ - warningRpcUrl: strings( - 'app_settings.url_associated_to_another_chain_id', - ), - validatedRpcURL: false, - warningChainId: strings('app_settings.unMatched_chain_name'), - }); - } + return this.setState({ + warningRpcUrl: strings( + 'app_settings.url_associated_to_another_chain_id', + ), + validatedRpcURL: false, + warningChainId: strings('app_settings.unMatched_chain_name'), + }); } this.validateRpcAndChainId(); @@ -980,14 +894,10 @@ export class NetworkSettings extends PureComponent { disabledByChainId = () => { const { chainId, validatedChainId, warningChainId } = this.state; - if (isNetworkUiRedesignEnabled()) { - return ( - !chainId || - (chainId && (!validatedChainId || warningChainId !== undefined)) - ); - } - if (!chainId) return true; - return validatedChainId && !!warningChainId; + return ( + !chainId || + (chainId && (!validatedChainId || warningChainId !== undefined)) + ); }; /** @@ -1011,7 +921,6 @@ export class NetworkSettings extends PureComponent { warningSymbol: undefined, warningName: undefined, }); - this.validateRpcUrl(this.state.rpcUrlForm); }; onRpcNameAdd = async (name) => { @@ -1096,6 +1005,10 @@ export class NetworkSettings extends PureComponent { this.getCurrentState(); }; + onRpcUrlValidationChange = (isValid) => { + this.setState({ validatedRpcURL: isValid }); + }; + onRpcUrlChangeWithName = async (url, failoverUrls, name, type) => { const nameToUse = name ?? type; const { addMode } = this.state; @@ -1468,9 +1381,7 @@ export class NetworkSettings extends PureComponent { const renderWarningChainId = () => { const CHAIN_LIST_URL = 'https://chainid.network/'; - const containerStyle = isNetworkUiRedesignEnabled() - ? styles.newWarningContainer - : styles.warningContainer; + const containerStyle = styles.newWarningContainer; if (warningChainId) { if (warningChainId === strings('app_settings.unMatched_chain_name')) { @@ -1568,66 +1479,22 @@ export class NetworkSettings extends PureComponent { }; const renderButtons = () => { - if (isNetworkUiRedesignEnabled()) { - return ( - - -