From 7bdfa73915340794e6534fb505cb0ee20be2ee42 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Tue, 5 Nov 2024 20:15:49 -0800 Subject: [PATCH 01/16] scaffold approvals --- app/core/SnapKeyring/SnapKeyring.ts | 55 ++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index ec5e9e5659f..ccd9dc94dea 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -3,6 +3,40 @@ import type { SnapController } from '@metamask/snaps-controllers'; import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; +const approvalType = 'snap_manageAccounts:showNameSnapAccount'; + +/** + * Show the account name suggestion confirmation dialog for a given Snap. + * + * @param snapId - Snap ID to show the account name suggestion dialog for. + * @param controllerMessenger - The controller messenger instance. + * @param accountNameSuggestion - Suggested name for the new account. + * @returns The user's confirmation result. + */ +export async function showAccountNameSuggestionDialog( + snapId: string, + controllerMessenger: SnapKeyringBuilderMessenger, + accountNameSuggestion: string, +): Promise<{ success: boolean; name?: string }> { + console.log('showAccountNameSuggestionDialog', accountNameSuggestion); + try { + const confirmationResult = (await controllerMessenger.call( + 'ApprovalController:addRequest', + { + origin: snapId, + type: approvalType, + requestData: { + snapSuggestedAccountName: accountNameSuggestion, + }, + }, + true, + )) as { success: boolean; name?: string }; + return confirmationResult; + } catch (e) { + throw new Error(`Error occurred while showing name account dialog.\n${e}`); + } +} + /** * Constructs a SnapKeyring builder with specified handlers for managing snap accounts. * - Here is the equivalent function on the extension: https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/snap-keyring/snap-keyring.ts#L111 @@ -54,11 +88,22 @@ export const snapKeyringBuilder = ( // TODO: Implement proper snap account confirmations. Currently, we are approving everything for testing purposes. Logger.log( `SnapKeyring: addAccount called with \n - - address: ${address} \n - - handleUserInput: ${handleUserInput} \n - - snapId: ${snapId} \n - - accountNameSuggestion: ${accountNameSuggestion} \n - - displayConfirmation: ${displayConfirmation}`, + - address: ${address} \n + - handleUserInput: ${handleUserInput} \n + - snapId: ${snapId} \n + - accountNameSuggestion: ${accountNameSuggestion} \n + - displayConfirmation: ${displayConfirmation}`, + ); + const accountNameConfirmationResult = + await showAccountNameSuggestionDialog( + snapId, + controllerMessenger, + accountNameSuggestion, + ); + + console.log( + 'accountNameConfirmationResult', + accountNameConfirmationResult, ); // Approve everything for now because we have not implemented snap account confirmations yet From f3c28b66f5937e955d48ff1e357b1550e64694cf Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Wed, 6 Nov 2024 15:45:32 -0800 Subject: [PATCH 02/16] confirmation modal is rendering --- .../SnapAccountCustomNameApproval.tsx | 33 +++++++++++++++++++ .../SnapAccountCustomNameApproval/index.ts | 3 ++ app/components/Nav/Main/RootRPCMethodsUI.js | 10 ++++++ app/core/RPCMethods/RPCMethodMiddleware.ts | 9 +++++ app/core/SnapKeyring/SnapKeyring.ts | 10 +++--- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx create mode 100644 app/components/Approvals/SnapAccountCustomNameApproval/index.ts diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx new file mode 100644 index 00000000000..1f21ce81882 --- /dev/null +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -0,0 +1,33 @@ +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +import React, { useEffect, useState } from 'react'; +import { Text } from 'react-native'; +import ApprovalModal from '../ApprovalModal'; +import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../core/RPCMethods/RPCMethodMiddleware'; +import { ApprovalRequest } from '@metamask/approval-controller'; +import { useSelector } from 'react-redux'; +import { selectSnapsMetadata } from '../../../selectors/snaps/snapController'; + +const SnapAccountCustomNameApproval = () => { + const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); + + console.log( + 'SnapKeyring: SnapAccountCustomNameApproval', + approvalRequest?.type, + ); + + return ( + + Snap Account Custom Name Approval + + ); +}; + +export default SnapAccountCustomNameApproval; +///: END:ONLY_INCLUDE_IF diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/index.ts b/app/components/Approvals/SnapAccountCustomNameApproval/index.ts new file mode 100644 index 00000000000..6e3deb8308d --- /dev/null +++ b/app/components/Approvals/SnapAccountCustomNameApproval/index.ts @@ -0,0 +1,3 @@ +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +export { default } from './SnapAccountCustomNameApproval'; +///: END:ONLY_INCLUDE_IF diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js index 086d3c3a61f..712919e682c 100644 --- a/app/components/Nav/Main/RootRPCMethodsUI.js +++ b/app/components/Nav/Main/RootRPCMethodsUI.js @@ -74,6 +74,9 @@ import { updateSwapsTransaction } from '../../../util/swaps/swaps-transactions'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import InstallSnapApproval from '../../Approvals/InstallSnapApproval'; ///: END:ONLY_INCLUDE_IF +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +import SnapAccountCustomNameApproval from '../../Approvals/SnapAccountCustomNameApproval'; +///: END:ONLY_INCLUDE_IF const hstInterface = new ethers.utils.Interface(abi); @@ -495,6 +498,13 @@ const RootRPCMethodsUI = (props) => { { ///: END:ONLY_INCLUDE_IF } + { + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + } + + { + ///: END:ONLY_INCLUDE_IF + } ); }; diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 033b42c9158..5b85a12ea11 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -51,6 +51,15 @@ const Engine = ImportedEngine as any; let appVersion = ''; +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = { + confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation', + confirmAccountRemoval: 'snap_manageAccounts:confirmAccountRemoval', + showSnapAccountRedirect: 'snap_manageAccounts:showSnapAccountRedirect', + showNameSnapAccount: 'snap_manageAccounts:showNameSnapAccount', +}; +///: END:ONLY_INCLUDE_IF + export enum ApprovalTypes { CONNECT_ACCOUNTS = 'CONNECT_ACCOUNTS', SIGN_MESSAGE = 'SIGN_MESSAGE', diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index ccd9dc94dea..dc7cb41357c 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -2,8 +2,7 @@ import { SnapKeyring } from '@metamask/eth-snap-keyring'; import type { SnapController } from '@metamask/snaps-controllers'; import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; - -const approvalType = 'snap_manageAccounts:showNameSnapAccount'; +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; /** * Show the account name suggestion confirmation dialog for a given Snap. @@ -18,13 +17,16 @@ export async function showAccountNameSuggestionDialog( controllerMessenger: SnapKeyringBuilderMessenger, accountNameSuggestion: string, ): Promise<{ success: boolean; name?: string }> { - console.log('showAccountNameSuggestionDialog', accountNameSuggestion); + console.log( + 'SnapKeyring: showAccountNameSuggestionDialog', + accountNameSuggestion, + ); try { const confirmationResult = (await controllerMessenger.call( 'ApprovalController:addRequest', { origin: snapId, - type: approvalType, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, requestData: { snapSuggestedAccountName: accountNameSuggestion, }, From 6a6034a285082e553eb7d0e299170dd9664c123e Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Wed, 6 Nov 2024 16:22:09 -0800 Subject: [PATCH 03/16] render custom account name approval --- ...SnapAccountCustomNameApproval.constants.ts | 8 +++ .../SnapAccountCustomNameApproval.styles.ts | 38 +++++++++++ .../SnapAccountCustomNameApproval.tsx | 63 ++++++++++++++++--- locales/languages/en.json | 5 ++ 4 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts create mode 100644 app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts new file mode 100644 index 00000000000..abec8f81752 --- /dev/null +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts @@ -0,0 +1,8 @@ +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +export const SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL = + 'snap-account-custom-name-approval'; +export const SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON = + 'snap-account-custom-name-approval-cancel-button'; +export const SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON = + 'snap-account-custom-name-approval-add-account-button'; +///: END:ONLY_INCLUDE_IF diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts new file mode 100644 index 00000000000..3c0ef7bfcc3 --- /dev/null +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts @@ -0,0 +1,38 @@ +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../util/theme/models'; +import Device from '../../../util/device'; + +/** + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + }); +}; + +export default styleSheet; +///: END:ONLY_INCLUDE_IF diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index 1f21ce81882..be4e60262e1 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -1,20 +1,50 @@ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import React, { useEffect, useState } from 'react'; -import { Text } from 'react-native'; +import React from 'react'; +import { View } from 'react-native'; import ApprovalModal from '../ApprovalModal'; import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../core/RPCMethods/RPCMethodMiddleware'; -import { ApprovalRequest } from '@metamask/approval-controller'; -import { useSelector } from 'react-redux'; -import { selectSnapsMetadata } from '../../../selectors/snaps/snapController'; +import { + SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, + SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL, + SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON, +} from './SnapAccountCustomNameApproval.constants'; +import styleSheet from './SnapAccountCustomNameApproval.styles'; +import { useStyles } from '../../hooks/useStyles'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../component-library/components/BottomSheets/BottomSheetFooter'; +import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; +import { strings } from '../../../../locales/i18n'; +import Text, { + TextVariant, +} from '../../../component-library/components/Texts/Text'; +import { + ButtonProps, + ButtonSize, + ButtonVariants, +} from '../../../component-library/components/Buttons/Button/Button.types'; const SnapAccountCustomNameApproval = () => { const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); - console.log( - 'SnapKeyring: SnapAccountCustomNameApproval', - approvalRequest?.type, - ); + const { styles } = useStyles(styleSheet, {}); + + const cancelButtonProps: ButtonProps = { + variant: ButtonVariants.Secondary, + label: strings('accountApproval.cancel'), + size: ButtonSize.Lg, + onPress: onReject, + testID: SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON, + }; + + const addAccountButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('snap_account_custom_name_approval.add_account_button'), + size: ButtonSize.Lg, + onPress: onConfirm, + testID: SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, + }; return ( { } onCancel={onReject} > - Snap Account Custom Name Approval + + + + Account name + + + + + ); }; diff --git a/locales/languages/en.json b/locales/languages/en.json index 9c371ef2bde..1bcfdb5532c 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3612,5 +3612,10 @@ "description": "Estimated changes for this transaction have been updated. Review them closely before proceeding.", "proceed": "Proceed", "reject": "Reject the transaction" + }, + "snap_account_custom_name_approval": { + "title": "Add account to MetaMask", + "input": "Account name", + "add_account_button": "Add account" } } From cd0c7e3bfb3a4f4f726b11f056b3da623c299cec Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Wed, 6 Nov 2024 17:04:41 -0800 Subject: [PATCH 04/16] render snap suggested name --- ...SnapAccountCustomNameApproval.constants.ts | 2 ++ .../SnapAccountCustomNameApproval.styles.ts | 13 ++++++-- .../SnapAccountCustomNameApproval.tsx | 30 +++++++++++++++---- locales/languages/en.json | 2 +- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts index abec8f81752..50b8da3111f 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.constants.ts @@ -5,4 +5,6 @@ export const SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON = 'snap-account-custom-name-approval-cancel-button'; export const SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON = 'snap-account-custom-name-approval-add-account-button'; +export const SNAP_ACCOUNT_CUSTOM_NAME_INPUT = + 'snap-account-custom-name-approval-input'; ///: END:ONLY_INCLUDE_IF diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts index 3c0ef7bfcc3..ce8c32319f5 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.styles.ts @@ -17,6 +17,7 @@ const styleSheet = (params: { theme: Theme }) => { root: { backgroundColor: colors.background.default, paddingTop: 24, + paddingHorizontal: 16, borderTopLeftRadius: 20, borderTopRightRadius: 20, minHeight: 200, @@ -27,9 +28,15 @@ const styleSheet = (params: { theme: Theme }) => { paddingVertical: 16, justifyContent: 'center', }, - description: { - textAlign: 'center', - paddingBottom: 16, + inputTitle: { + textAlign: 'left', + }, + input: { + borderWidth: 1, + borderColor: colors.border.default, + borderRadius: 4, + padding: 10, + marginVertical: 10, }, }); }; diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index be4e60262e1..73f94dac0c9 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -1,6 +1,6 @@ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import React from 'react'; -import { View } from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { TextInput, View } from 'react-native'; import ApprovalModal from '../ApprovalModal'; import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../core/RPCMethods/RPCMethodMiddleware'; @@ -8,6 +8,7 @@ import { SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL, SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON, + SNAP_ACCOUNT_CUSTOM_NAME_INPUT, } from './SnapAccountCustomNameApproval.constants'; import styleSheet from './SnapAccountCustomNameApproval.styles'; import { useStyles } from '../../hooks/useStyles'; @@ -27,9 +28,22 @@ import { const SnapAccountCustomNameApproval = () => { const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); + // console.log( + // 'SnapKeyring: SnapAccountCustomNameApproval', + // JSON.stringify(approvalRequest, null, 2), + // ); + const [accountName, setAccountName] = useState(''); + + useEffect(() => { + setAccountName(approvalRequest?.requestData.snapSuggestedAccountName || ''); + }, [approvalRequest]); const { styles } = useStyles(styleSheet, {}); + const onAddAccountPressed = useCallback(() => { + onConfirm(); + }, [onConfirm]); + const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, label: strings('accountApproval.cancel'), @@ -42,7 +56,7 @@ const SnapAccountCustomNameApproval = () => { variant: ButtonVariants.Primary, label: strings('snap_account_custom_name_approval.add_account_button'), size: ButtonSize.Lg, - onPress: onConfirm, + onPress: onAddAccountPressed, testID: SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, }; @@ -58,9 +72,15 @@ const SnapAccountCustomNameApproval = () => { - - Account name + + {strings('snap_account_custom_name_approval.input_title')} + setAccountName(text)} + testID={SNAP_ACCOUNT_CUSTOM_NAME_INPUT} + /> Date: Wed, 6 Nov 2024 18:43:09 -0800 Subject: [PATCH 05/16] pass names back to snap keyring --- .../SnapAccountCustomNameApproval.tsx | 12 +-- app/core/SnapKeyring/SnapKeyring.ts | 88 ++++++++++++------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index 73f94dac0c9..4f8b5dcaa22 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -28,10 +28,10 @@ import { const SnapAccountCustomNameApproval = () => { const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); - // console.log( - // 'SnapKeyring: SnapAccountCustomNameApproval', - // JSON.stringify(approvalRequest, null, 2), - // ); + console.log( + 'SnapKeyring: SnapAccountCustomNameApproval', + JSON.stringify(approvalRequest, null, 2), + ); const [accountName, setAccountName] = useState(''); useEffect(() => { @@ -41,8 +41,8 @@ const SnapAccountCustomNameApproval = () => { const { styles } = useStyles(styleSheet, {}); const onAddAccountPressed = useCallback(() => { - onConfirm(); - }, [onConfirm]); + onConfirm(undefined, { success: true, name: accountName }); + }, [accountName, onConfirm]); const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index dc7cb41357c..4c67d5db4b2 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -4,6 +4,11 @@ import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; +interface CreateAccountConfirmationResult { + success: boolean; + name?: string; +} + /** * Show the account name suggestion confirmation dialog for a given Snap. * @@ -16,11 +21,7 @@ export async function showAccountNameSuggestionDialog( snapId: string, controllerMessenger: SnapKeyringBuilderMessenger, accountNameSuggestion: string, -): Promise<{ success: boolean; name?: string }> { - console.log( - 'SnapKeyring: showAccountNameSuggestionDialog', - accountNameSuggestion, - ); +): Promise { try { const confirmationResult = (await controllerMessenger.call( 'ApprovalController:addRequest', @@ -32,8 +33,15 @@ export async function showAccountNameSuggestionDialog( }, }, true, - )) as { success: boolean; name?: string }; - return confirmationResult; + )) as CreateAccountConfirmationResult; + + if (confirmationResult) { + return { + success: confirmationResult.success, + name: confirmationResult.name, + }; + } + return { success: false }; } catch (e) { throw new Error(`Error occurred while showing name account dialog.\n${e}`); } @@ -96,34 +104,52 @@ export const snapKeyringBuilder = ( - accountNameSuggestion: ${accountNameSuggestion} \n - displayConfirmation: ${displayConfirmation}`, ); - const accountNameConfirmationResult = - await showAccountNameSuggestionDialog( - snapId, - controllerMessenger, - accountNameSuggestion, - ); - console.log( - 'accountNameConfirmationResult', - accountNameConfirmationResult, + const { id: addAccountFlowId } = controllerMessenger.call( + 'ApprovalController:startFlow', ); - // Approve everything for now because we have not implemented snap account confirmations yet - await handleUserInput(true); - await persistKeyringHelper(); - const account = controllerMessenger.call( - 'AccountsController:getAccountByAddress', - address, - ); - if (!account) { - throw new Error(`Internal account not found for address: ${address}`); - } + try { + const accountNameConfirmationResult = + await showAccountNameSuggestionDialog( + snapId, + controllerMessenger, + accountNameSuggestion, + ); - // Set the selected account to the new account - controllerMessenger.call( - 'AccountsController:setSelectedAccount', - account.id, - ); + console.log( + 'SnapKeyring: accountNameConfirmationResult', + accountNameConfirmationResult, + ); + + // Approve everything for now because we have not implemented snap account confirmations yet + await handleUserInput(true); + await persistKeyringHelper(); + const account = controllerMessenger.call( + 'AccountsController:getAccountByAddress', + address, + ); + if (!account) { + throw new Error( + `Internal account not found for address: ${address}`, + ); + } + + // Set the selected account to the new account + controllerMessenger.call( + 'AccountsController:setSelectedAccount', + account.id, + ); + } catch (error) { + console.log('SnapKeyring: addAccount error', error); + controllerMessenger.call('ApprovalController:endFlow', { + id: addAccountFlowId, + }); + } finally { + controllerMessenger.call('ApprovalController:endFlow', { + id: addAccountFlowId, + }); + } }, removeAccount: async ( From 34c1a119b8f1f9ab976c8a5ffb8e0c4092664901 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Wed, 6 Nov 2024 20:08:52 -0800 Subject: [PATCH 06/16] set custom account names and check for duplicates --- .../SnapAccountCustomNameApproval.tsx | 45 +++++++++++++++---- app/core/SnapKeyring/SnapKeyring.ts | 42 ++++++++++------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index 4f8b5dcaa22..23783918bb9 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -25,25 +25,54 @@ import { ButtonSize, ButtonVariants, } from '../../../component-library/components/Buttons/Button/Button.types'; +import { useSelector } from 'react-redux'; +import { selectInternalAccounts } from '../../../selectors/accountsController'; +import { InternalAccount } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import Engine from '../../../core/Engine'; const SnapAccountCustomNameApproval = () => { const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); - console.log( - 'SnapKeyring: SnapAccountCustomNameApproval', - JSON.stringify(approvalRequest, null, 2), - ); + const internalAccounts = useSelector(selectInternalAccounts); const [accountName, setAccountName] = useState(''); - useEffect(() => { - setAccountName(approvalRequest?.requestData.snapSuggestedAccountName || ''); - }, [approvalRequest]); - const { styles } = useStyles(styleSheet, {}); const onAddAccountPressed = useCallback(() => { onConfirm(undefined, { success: true, name: accountName }); }, [accountName, onConfirm]); + console.log( + 'SnapKeyring: SnapAccountCustomNameApproval', + JSON.stringify(approvalRequest, null, 2), + ); + + function isNameTaken(name: string, accounts: InternalAccount[]) { + return accounts.some((account) => account.metadata.name === name); + } + + useEffect(() => { + if (approvalRequest?.requestData.snapSuggestedAccountName) { + let suffix = 1; + const snapSuggestedAccountName = + approvalRequest?.requestData.snapSuggestedAccountName; + let candidateName = approvalRequest.requestData.snapSuggestedAccountName; + + // Keep incrementing suffix until we find an available name + while (isNameTaken(candidateName, internalAccounts)) { + suffix += 1; + candidateName = `${snapSuggestedAccountName} ${suffix}`; + } + setAccountName(candidateName); + } else { + const nextAccountName = + Engine.context.AccountsController.getNextAvailableAccountName( + KeyringTypes.snap, + ); + setAccountName(nextAccountName); + } + }, [approvalRequest, internalAccounts]); + const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, label: strings('accountApproval.cancel'), diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index 4c67d5db4b2..969e62b85c7 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -122,24 +122,34 @@ export const snapKeyringBuilder = ( accountNameConfirmationResult, ); - // Approve everything for now because we have not implemented snap account confirmations yet - await handleUserInput(true); - await persistKeyringHelper(); - const account = controllerMessenger.call( - 'AccountsController:getAccountByAddress', - address, - ); - if (!account) { - throw new Error( - `Internal account not found for address: ${address}`, + if (accountNameConfirmationResult.success) { + // Approve everything for now because we have not implemented snap account confirmations yet + await handleUserInput(true); + await persistKeyringHelper(); + const account = controllerMessenger.call( + 'AccountsController:getAccountByAddress', + address, ); - } + if (!account) { + throw new Error( + `Internal account not found for address: ${address}`, + ); + } - // Set the selected account to the new account - controllerMessenger.call( - 'AccountsController:setSelectedAccount', - account.id, - ); + // Set the selected account to the new account + controllerMessenger.call( + 'AccountsController:setSelectedAccount', + account.id, + ); + + if (accountNameConfirmationResult.name) { + controllerMessenger.call( + 'AccountsController:setAccountName', + account.id, + accountNameConfirmationResult.name, + ); + } + } } catch (error) { console.log('SnapKeyring: addAccount error', error); controllerMessenger.call('ApprovalController:endFlow', { From f48a592852cb801b090685058a3089ba220f80b4 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Thu, 7 Nov 2024 15:14:07 -0800 Subject: [PATCH 07/16] check if account name is taken in the front end --- .../SnapAccountCustomNameApproval.tsx | 34 ++++++++++++++----- locales/languages/en.json | 3 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index 23783918bb9..fcbd228cf9c 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -18,6 +18,7 @@ import BottomSheetFooter, { import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; import { strings } from '../../../../locales/i18n'; import Text, { + TextColor, TextVariant, } from '../../../component-library/components/Texts/Text'; import { @@ -27,7 +28,6 @@ import { } from '../../../component-library/components/Buttons/Button/Button.types'; import { useSelector } from 'react-redux'; import { selectInternalAccounts } from '../../../selectors/accountsController'; -import { InternalAccount } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import Engine from '../../../core/Engine'; @@ -35,21 +35,26 @@ const SnapAccountCustomNameApproval = () => { const { approvalRequest, onConfirm, onReject } = useApprovalRequest(); const internalAccounts = useSelector(selectInternalAccounts); const [accountName, setAccountName] = useState(''); + const [isNameTaken, setIsNameTaken] = useState(false); const { styles } = useStyles(styleSheet, {}); const onAddAccountPressed = useCallback(() => { - onConfirm(undefined, { success: true, name: accountName }); - }, [accountName, onConfirm]); + if (!isNameTaken) { + onConfirm(undefined, { success: true, name: accountName }); + } + }, [accountName, onConfirm, isNameTaken]); console.log( 'SnapKeyring: SnapAccountCustomNameApproval', JSON.stringify(approvalRequest, null, 2), ); - function isNameTaken(name: string, accounts: InternalAccount[]) { - return accounts.some((account) => account.metadata.name === name); - } + const checkIfNameTaken = useCallback( + (name: string) => + internalAccounts.some((account) => account.metadata.name === name), + [internalAccounts], + ); useEffect(() => { if (approvalRequest?.requestData.snapSuggestedAccountName) { @@ -59,7 +64,7 @@ const SnapAccountCustomNameApproval = () => { let candidateName = approvalRequest.requestData.snapSuggestedAccountName; // Keep incrementing suffix until we find an available name - while (isNameTaken(candidateName, internalAccounts)) { + while (checkIfNameTaken(candidateName)) { suffix += 1; candidateName = `${snapSuggestedAccountName} ${suffix}`; } @@ -71,7 +76,7 @@ const SnapAccountCustomNameApproval = () => { ); setAccountName(nextAccountName); } - }, [approvalRequest, internalAccounts]); + }, [approvalRequest, internalAccounts, checkIfNameTaken]); const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, @@ -87,6 +92,12 @@ const SnapAccountCustomNameApproval = () => { size: ButtonSize.Lg, onPress: onAddAccountPressed, testID: SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, + isDisabled: isNameTaken, + }; + + const handleNameChange = (text: string) => { + setAccountName(text); + setIsNameTaken(checkIfNameTaken(text)); }; return ( @@ -107,9 +118,14 @@ const SnapAccountCustomNameApproval = () => { setAccountName(text)} + onChangeText={handleNameChange} testID={SNAP_ACCOUNT_CUSTOM_NAME_INPUT} /> + {isNameTaken && ( + + {strings('snap_account_custom_name_approval.name_taken_message')} + + )} Date: Thu, 7 Nov 2024 18:22:58 -0800 Subject: [PATCH 08/16] better error handling --- .../SnapAccountCustomNameApproval.tsx | 5 -- app/core/SnapKeyring/SnapKeyring.ts | 74 +++++++++---------- 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index fcbd228cf9c..d7cf475a1bd 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -45,11 +45,6 @@ const SnapAccountCustomNameApproval = () => { } }, [accountName, onConfirm, isNameTaken]); - console.log( - 'SnapKeyring: SnapAccountCustomNameApproval', - JSON.stringify(approvalRequest, null, 2), - ); - const checkIfNameTaken = useCallback( (name: string) => internalAccounts.some((account) => account.metadata.name === name), diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index 969e62b85c7..a1666c36eaa 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -93,18 +93,8 @@ export const snapKeyringBuilder = ( snapId: string, handleUserInput: (accepted: boolean) => Promise, accountNameSuggestion = '', - displayConfirmation = false, + _displayConfirmation = false, ) => { - // TODO: Implement proper snap account confirmations. Currently, we are approving everything for testing purposes. - Logger.log( - `SnapKeyring: addAccount called with \n - - address: ${address} \n - - handleUserInput: ${handleUserInput} \n - - snapId: ${snapId} \n - - accountNameSuggestion: ${accountNameSuggestion} \n - - displayConfirmation: ${displayConfirmation}`, - ); - const { id: addAccountFlowId } = controllerMessenger.call( 'ApprovalController:startFlow', ); @@ -117,44 +107,46 @@ export const snapKeyringBuilder = ( accountNameSuggestion, ); - console.log( - 'SnapKeyring: accountNameConfirmationResult', - accountNameConfirmationResult, - ); - if (accountNameConfirmationResult.success) { - // Approve everything for now because we have not implemented snap account confirmations yet - await handleUserInput(true); - await persistKeyringHelper(); - const account = controllerMessenger.call( - 'AccountsController:getAccountByAddress', - address, - ); - if (!account) { - throw new Error( - `Internal account not found for address: ${address}`, + try { + await persistKeyringHelper(); + await handleUserInput(accountNameConfirmationResult.success); + const account = controllerMessenger.call( + 'AccountsController:getAccountByAddress', + address, ); - } + if (!account) { + throw new Error( + `Internal account not found for address: ${address}`, + ); + } - // Set the selected account to the new account - controllerMessenger.call( - 'AccountsController:setSelectedAccount', - account.id, - ); - - if (accountNameConfirmationResult.name) { + // Set the selected account to the new account controllerMessenger.call( - 'AccountsController:setAccountName', + 'AccountsController:setSelectedAccount', account.id, - accountNameConfirmationResult.name, + ); + + if (accountNameConfirmationResult.name) { + controllerMessenger.call( + 'AccountsController:setAccountName', + account.id, + accountNameConfirmationResult.name, + ); + } + } catch (e) { + // Error occurred while naming the account + const error = (e as Error).message; + throw new Error( + `Error occurred while creating snap account: ${error}`, ); } + } else { + // User has cancelled account creation so remove the account from the keyring + await handleUserInput(accountNameConfirmationResult?.success); + + throw new Error('User denied account creation'); } - } catch (error) { - console.log('SnapKeyring: addAccount error', error); - controllerMessenger.call('ApprovalController:endFlow', { - id: addAccountFlowId, - }); } finally { controllerMessenger.call('ApprovalController:endFlow', { id: addAccountFlowId, From 64448f335ebb4ff0bf158f9d816337fb3c2536d6 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Thu, 7 Nov 2024 18:58:02 -0800 Subject: [PATCH 09/16] wip scaffold success/error messages --- app/core/SnapKeyring/SnapKeyring.ts | 37 ++++++++++++ app/core/SnapKeyring/utils/showResult.ts | 74 ++++++++++++++++++++++++ locales/languages/en.json | 6 ++ 3 files changed, 117 insertions(+) create mode 100644 app/core/SnapKeyring/utils/showResult.ts diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index a1666c36eaa..78c4ac951c2 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -3,6 +3,9 @@ import type { SnapController } from '@metamask/snaps-controllers'; import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; +import { IconName } from '../../component-library/components/Icons/Icon'; +import { showError, showSuccess } from './utils/showResult'; +import { strings } from '../../../locales/i18n'; interface CreateAccountConfirmationResult { success: boolean; @@ -95,6 +98,8 @@ export const snapKeyringBuilder = ( accountNameSuggestion = '', _displayConfirmation = false, ) => { + const learnMoreLink = + 'https://support.metamask.io/managing-my-wallet/accounts-and-addresses/how-to-add-accounts-in-your-wallet/'; const { id: addAccountFlowId } = controllerMessenger.call( 'ApprovalController:startFlow', ); @@ -134,9 +139,41 @@ export const snapKeyringBuilder = ( accountNameConfirmationResult.name, ); } + await showSuccess( + controllerMessenger, + snapId, + { + icon: IconName.UserCircleAdd, + title: strings('snap_keyring.snap_account_created'), + }, + { + message: strings( + 'snap_keyring.snap_account_created_description', + ), + address, + learnMoreLink, + }, + ); } catch (e) { // Error occurred while naming the account const error = (e as Error).message; + + await showError( + controllerMessenger, + snapId, + { + icon: IconName.UserCircleAdd, + title: strings('snap_keyring.snap_account_creation_failed'), + }, + { + message: strings( + 'snap_keyring.snap_account_creation_failed_description', + { snapName: snapId }, + ), + learnMoreLink, + error, + }, + ); throw new Error( `Error occurred while creating snap account: ${error}`, ); diff --git a/app/core/SnapKeyring/utils/showResult.ts b/app/core/SnapKeyring/utils/showResult.ts new file mode 100644 index 00000000000..8e2a629ee6b --- /dev/null +++ b/app/core/SnapKeyring/utils/showResult.ts @@ -0,0 +1,74 @@ +import type { ErrorResult } from '@metamask/approval-controller'; +import { SnapKeyringBuilderMessenger } from '../types'; +import { IconName } from '../../../component-library/components/Icons/Icon'; + +/** + * Options for result pages. + */ +export interface ResultComponentOptions { + /** + * The title to display above the message. Shown by default but can be hidden with `null`. + */ + title: string | null; + + /** + * The icon to display in the page. Shown by default but can be hidden with `null`. + */ + icon: IconName | null; +} + +/** + * Shows an error result page. + * + * @param controllerMessenger - The controller messenger instance. + * @param snapId - The Snap unique id. + * @param opts - The result component options (title, icon). + * @param properties - The properties used by SnapAccountErrorMessage component. + * @returns Returns a promise that resolves once the user clicks the confirm + * button. + */ +export const showError = ( + controllerMessenger: SnapKeyringBuilderMessenger, + snapId: string, + opts: ResultComponentOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties: Record, +): Promise => + controllerMessenger.call('ApprovalController:showError', { + header: [`${snapId}`], + title: opts.title, + icon: opts.icon, + error: { + key: 'snapAccountErrorMessage', + name: 'SnapAccountErrorMessage', + properties, + }, + }); + +/** + * Shows a success result page. + * + * @param controllerMessenger - The controller messenger instance. + * @param snapId - The Snap unique id. + * @param opts - The result component options (title, icon). + * @param properties - The properties used by SnapAccountSuccessMessage component. + * @returns Returns a promise that resolves once the user clicks the confirm + * button. + */ +export const showSuccess = ( + controllerMessenger: SnapKeyringBuilderMessenger, + snapId: string, + opts: ResultComponentOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties: Record, +): Promise => + controllerMessenger.call('ApprovalController:showSuccess', { + header: [`${snapId}`], + title: opts.title, + icon: opts.icon, + message: { + key: 'snapAccountSuccessMessage', + name: 'SnapAccountSuccessMessage', + properties, + }, + }); diff --git a/locales/languages/en.json b/locales/languages/en.json index 742483d8bcc..e99dc13df62 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3618,5 +3618,11 @@ "input_title": "Account name", "add_account_button": "Add account", "name_taken_message": "This account name already exists" + }, + "snap_keyring": { + "snap_account_created": "Account created", + "snap_account_created_description": "Your new account is ready to use!", + "snap_account_creation_failed": "Account creation failed", + "snap_account_creation_failed_description": "{{snapName}} didn't manage to create an account for you." } } From c9b5d6190faf43862a9452f2c9fd0f422782abd0 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 8 Nov 2024 13:32:08 -0800 Subject: [PATCH 10/16] remove error handling --- app/core/SnapKeyring/SnapKeyring.ts | 37 ------------ app/core/SnapKeyring/utils/showResult.ts | 74 ------------------------ locales/languages/en.json | 6 -- 3 files changed, 117 deletions(-) delete mode 100644 app/core/SnapKeyring/utils/showResult.ts diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index 78c4ac951c2..a1666c36eaa 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -3,9 +3,6 @@ import type { SnapController } from '@metamask/snaps-controllers'; import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; -import { IconName } from '../../component-library/components/Icons/Icon'; -import { showError, showSuccess } from './utils/showResult'; -import { strings } from '../../../locales/i18n'; interface CreateAccountConfirmationResult { success: boolean; @@ -98,8 +95,6 @@ export const snapKeyringBuilder = ( accountNameSuggestion = '', _displayConfirmation = false, ) => { - const learnMoreLink = - 'https://support.metamask.io/managing-my-wallet/accounts-and-addresses/how-to-add-accounts-in-your-wallet/'; const { id: addAccountFlowId } = controllerMessenger.call( 'ApprovalController:startFlow', ); @@ -139,41 +134,9 @@ export const snapKeyringBuilder = ( accountNameConfirmationResult.name, ); } - await showSuccess( - controllerMessenger, - snapId, - { - icon: IconName.UserCircleAdd, - title: strings('snap_keyring.snap_account_created'), - }, - { - message: strings( - 'snap_keyring.snap_account_created_description', - ), - address, - learnMoreLink, - }, - ); } catch (e) { // Error occurred while naming the account const error = (e as Error).message; - - await showError( - controllerMessenger, - snapId, - { - icon: IconName.UserCircleAdd, - title: strings('snap_keyring.snap_account_creation_failed'), - }, - { - message: strings( - 'snap_keyring.snap_account_creation_failed_description', - { snapName: snapId }, - ), - learnMoreLink, - error, - }, - ); throw new Error( `Error occurred while creating snap account: ${error}`, ); diff --git a/app/core/SnapKeyring/utils/showResult.ts b/app/core/SnapKeyring/utils/showResult.ts deleted file mode 100644 index 8e2a629ee6b..00000000000 --- a/app/core/SnapKeyring/utils/showResult.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ErrorResult } from '@metamask/approval-controller'; -import { SnapKeyringBuilderMessenger } from '../types'; -import { IconName } from '../../../component-library/components/Icons/Icon'; - -/** - * Options for result pages. - */ -export interface ResultComponentOptions { - /** - * The title to display above the message. Shown by default but can be hidden with `null`. - */ - title: string | null; - - /** - * The icon to display in the page. Shown by default but can be hidden with `null`. - */ - icon: IconName | null; -} - -/** - * Shows an error result page. - * - * @param controllerMessenger - The controller messenger instance. - * @param snapId - The Snap unique id. - * @param opts - The result component options (title, icon). - * @param properties - The properties used by SnapAccountErrorMessage component. - * @returns Returns a promise that resolves once the user clicks the confirm - * button. - */ -export const showError = ( - controllerMessenger: SnapKeyringBuilderMessenger, - snapId: string, - opts: ResultComponentOptions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - properties: Record, -): Promise => - controllerMessenger.call('ApprovalController:showError', { - header: [`${snapId}`], - title: opts.title, - icon: opts.icon, - error: { - key: 'snapAccountErrorMessage', - name: 'SnapAccountErrorMessage', - properties, - }, - }); - -/** - * Shows a success result page. - * - * @param controllerMessenger - The controller messenger instance. - * @param snapId - The Snap unique id. - * @param opts - The result component options (title, icon). - * @param properties - The properties used by SnapAccountSuccessMessage component. - * @returns Returns a promise that resolves once the user clicks the confirm - * button. - */ -export const showSuccess = ( - controllerMessenger: SnapKeyringBuilderMessenger, - snapId: string, - opts: ResultComponentOptions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - properties: Record, -): Promise => - controllerMessenger.call('ApprovalController:showSuccess', { - header: [`${snapId}`], - title: opts.title, - icon: opts.icon, - message: { - key: 'snapAccountSuccessMessage', - name: 'SnapAccountSuccessMessage', - properties, - }, - }); diff --git a/locales/languages/en.json b/locales/languages/en.json index e99dc13df62..742483d8bcc 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3618,11 +3618,5 @@ "input_title": "Account name", "add_account_button": "Add account", "name_taken_message": "This account name already exists" - }, - "snap_keyring": { - "snap_account_created": "Account created", - "snap_account_created_description": "Your new account is ready to use!", - "snap_account_creation_failed": "Account creation failed", - "snap_account_creation_failed_description": "{{snapName}} didn't manage to create an account for you." } } From ec3a674c47b96aacc30573e9edfd13cd9368312e Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 8 Nov 2024 15:06:28 -0800 Subject: [PATCH 11/16] improve snap keyring tests --- app/core/RPCMethods/RPCMethodMiddleware.ts | 3 - app/core/SnapKeyring/SnapKeyring.test.ts | 77 ++++++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 5b85a12ea11..4f3580e0d92 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -53,9 +53,6 @@ let appVersion = ''; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = { - confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation', - confirmAccountRemoval: 'snap_manageAccounts:confirmAccountRemoval', - showSnapAccountRedirect: 'snap_manageAccounts:showSnapAccountRedirect', showNameSnapAccount: 'snap_manageAccounts:showNameSnapAccount', }; ///: END:ONLY_INCLUDE_IF diff --git a/app/core/SnapKeyring/SnapKeyring.test.ts b/app/core/SnapKeyring/SnapKeyring.test.ts index fad726e0294..0e8a181830b 100644 --- a/app/core/SnapKeyring/SnapKeyring.test.ts +++ b/app/core/SnapKeyring/SnapKeyring.test.ts @@ -10,7 +10,11 @@ import { SnapKeyringBuilderMessenger, } from './types'; import { SnapId } from '@metamask/snaps-sdk'; +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; +const mockAddRequest = jest.fn(); +const mockStartFlow = jest.fn(); +const mockEndFlow = jest.fn(); const mockGetAccounts = jest.fn(); const mockSnapId: SnapId = 'snapId' as SnapId; const mockSnapName = 'mock-snap'; @@ -19,7 +23,9 @@ const mockPersisKeyringHelper = jest.fn(); const mockSetSelectedAccount = jest.fn(); const mockRemoveAccountHelper = jest.fn(); const mockGetAccountByAddress = jest.fn(); +const mockSetAccountName = jest.fn(); +const mockFlowId = '123'; const address = '0x2a4d4b667D5f12C3F9Bf8F14a7B9f8D8d9b8c8fA'; const accountNameSuggestion = 'Suggested Account Name'; const mockAccount = { @@ -77,15 +83,20 @@ const createControllerMessenger = ({ const [actionType, ...params]: any[] = args; switch (actionType) { + case 'ApprovalController:startFlow': + return mockStartFlow.mockReturnValue({ id: mockFlowId })(); + case 'ApprovalController:addRequest': + return mockAddRequest(params); + case 'ApprovalController:endFlow': + return mockEndFlow.mockReturnValue(true)(params); case 'KeyringController:getAccounts': return mockGetAccounts.mockResolvedValue([])(); - case 'AccountsController:getAccountByAddress': return mockGetAccountByAddress.mockReturnValue(account)(params); - case 'AccountsController:setSelectedAccount': return mockSetSelectedAccount(params); - + case 'AccountsController:setAccountName': + return mockSetAccountName.mockReturnValue(null)(params); default: throw new Error( `MOCK_FAIL - unsupported messenger call: ${actionType}`, @@ -110,6 +121,9 @@ describe('Snap Keyring Methods', () => { }); describe('addAccount', () => { + beforeEach(() => { + mockAddRequest.mockReturnValue(true).mockReturnValue({ success: true }); + }); afterEach(() => { jest.resetAllMocks(); }); @@ -120,14 +134,69 @@ describe('Snap Keyring Methods', () => { method: KeyringEvent.AccountCreated, params: { account: mockAccount, - displayConfirmation: true, + displayConfirmation: false, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: '', + }, + }, + true, + ]); + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockSetAccountName).not.toHaveBeenCalled(); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('handles account creation with user defined name', async () => { + const mockNameSuggestion = 'suggested name'; + mockAddRequest.mockReturnValueOnce({ + success: true, + name: mockNameSuggestion, + }); + const builder = createSnapKeyringBuilder(); + await builder().handleKeyringSnapMessage(mockSnapId, { + method: KeyringEvent.AccountCreated, + params: { + account: mockAccount, + displayConfirmation: false, + accountNameSuggestion: mockNameSuggestion, }, }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: mockNameSuggestion, + }, + }, + true, + ]); expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); expect(mockGetAccountByAddress).toHaveBeenCalledWith([ mockAccount.address.toLowerCase(), ]); + expect(mockSetAccountName).toHaveBeenCalledTimes(1); + expect(mockSetAccountName).toHaveBeenCalledWith([ + mockAccount.id, + mockNameSuggestion, + ]); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); }); }); }); From 1fe221506e53c7d01fc0431edeed19debc156759 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 8 Nov 2024 17:49:13 -0800 Subject: [PATCH 12/16] test SnapAccountCustomNameApproval --- .../SnapAccountCustomNameApproval.test.tsx | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 app/components/Approvals/SnapAccountCustomNameApproval/test/SnapAccountCustomNameApproval.test.tsx diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/test/SnapAccountCustomNameApproval.test.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/test/SnapAccountCustomNameApproval.test.tsx new file mode 100644 index 00000000000..edff596c50f --- /dev/null +++ b/app/components/Approvals/SnapAccountCustomNameApproval/test/SnapAccountCustomNameApproval.test.tsx @@ -0,0 +1,297 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import { + SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL, + SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON, + SNAP_ACCOUNT_CUSTOM_NAME_INPUT, + SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON, +} from '../SnapAccountCustomNameApproval.constants'; +import { ApprovalRequest } from '@metamask/approval-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../core/RPCMethods/RPCMethodMiddleware'; +import SnapAccountCustomNameApproval from '../SnapAccountCustomNameApproval'; +import renderWithProvider, { + DeepPartial, +} from '../../../../util/test/renderWithProvider'; +import useApprovalRequest from '../../../Views/confirmations/hooks/useApprovalRequest'; +import Engine from '../../../../core/Engine'; +import { RootState } from '../../../../reducers'; +import { + MOCK_ACCOUNTS_CONTROLLER_STATE, + MOCK_ADDRESS_1, +} from '../../../../util/test/accountsControllerTestUtils'; + +jest.mock('../../../Views/confirmations/hooks/useApprovalRequest'); + +jest.mock('../../../../core/Engine', () => { + const { MOCK_ADDRESS_1: mockAddress1 } = jest.requireActual( + '../../../../util/test/accountsControllerTestUtils', + ); + return { + context: { + AccountsController: { + getNextAvailableAccountName: jest.fn(), + }, + KeyringController: { + state: { + keyrings: [ + { + accounts: [mockAddress1], + }, + ], + }, + }, + }, + }; +}); + +const onConfirm = jest.fn(); +const onReject = jest.fn(); + +// TODO: Replace "any" with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockApprovalRequest = (approvalRequest?: ApprovalRequest) => { + ( + useApprovalRequest as jest.MockedFn + ).mockReturnValue({ + approvalRequest, + onConfirm, + onReject, + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); +}; + +describe('SnapAccountCustomNameApproval', () => { + const getNextAvailableAccountNameMock = ( + Engine.context.AccountsController.getNextAvailableAccountName as jest.Mock + ).mockImplementation(() => 'Snap Account 3'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const initialState: DeepPartial = { + engine: { + backgroundState: { + AccountsController: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE, + }, + KeyringController: { + keyrings: [ + { + accounts: [MOCK_ADDRESS_1], + }, + ], + }, + }, + }, + }; + + it('renders correctly when approvalRequest is for showNameSnapAccount', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'New Account', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const approvalModal = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL); + expect(approvalModal).toBeDefined(); + }); + + it('initializes accountName with snapSuggestedAccountName when provided and name is not taken', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'Unique Account', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const input = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_INPUT); + expect(input.props.value).toBe('Unique Account'); + }); + + it('increments suffix when snapSuggestedAccountName is taken', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'Account 1', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const input = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_INPUT); + expect(input.props.value).toBe('Account 1 2'); + }); + + it('initializes accountName with next available account name when snapSuggestedAccountName is not provided', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: {}, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + getNextAvailableAccountNameMock.mockReturnValue('Snap Account 3'); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + expect(getNextAvailableAccountNameMock).toHaveBeenCalledWith( + KeyringTypes.snap, + ); + + const input = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_INPUT); + expect(input.props.value).toBe('Snap Account 3'); + }); + + it('shows error message and disables "Add Account" button when name is taken', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'Unique Account', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId, getByText } = renderWithProvider( + , + { state: initialState }, + ); + + const input = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_INPUT); + fireEvent.changeText(input, 'Account 2'); + + // Check that error message is displayed + expect(getByText('This account name already exists')).toBeDefined(); + + const addButton = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON); + expect(addButton.props.disabled).toBe(true); + }); + + it('calls onConfirm with account name when "Add Account" button is pressed and name is not taken', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'Unique Account', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const addButton = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_ADD_ACCOUNT_BUTTON); + fireEvent.press(addButton); + + expect(onConfirm).toHaveBeenCalledWith(undefined, { + success: true, + name: 'Unique Account', + }); + }); + + it('calls onReject when "Cancel" button is pressed', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: 'Unique Account', + }, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { getByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const cancelButton = getByTestId(SNAP_ACCOUNT_CUSTOM_NAME_CANCEL_BUTTON); + fireEvent.press(cancelButton); + + expect(onReject).toHaveBeenCalled(); + }); + + it('does not render when approvalRequest type is not showNameSnapAccount', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockApprovalRequestData: ApprovalRequest = { + id: '1', + origin: 'metamask', + time: Date.now(), + type: 'some_other_type', + requestData: {}, + requestState: null, + expectsResult: false, + }; + mockApprovalRequest(mockApprovalRequestData); + + const { queryByTestId } = renderWithProvider( + , + { state: initialState }, + ); + + const approvalModal = queryByTestId(SNAP_ACCOUNT_CUSTOM_NAME_APPROVAL); + expect(approvalModal).toBeNull(); + }); +}); From 5dc0e1133ad6dfd3d249364f3d7a994888e213e9 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Thu, 14 Nov 2024 14:45:26 -0800 Subject: [PATCH 13/16] move functions out of useeffect --- .../SnapAccountCustomNameApproval.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index d7cf475a1bd..049c45d690e 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -51,27 +51,37 @@ const SnapAccountCustomNameApproval = () => { [internalAccounts], ); - useEffect(() => { - if (approvalRequest?.requestData.snapSuggestedAccountName) { + const generateUniqueNameWithSuffix = useCallback( + (baseName: string): string => { let suffix = 1; - const snapSuggestedAccountName = - approvalRequest?.requestData.snapSuggestedAccountName; - let candidateName = approvalRequest.requestData.snapSuggestedAccountName; + let candidateName = baseName; - // Keep incrementing suffix until we find an available name while (checkIfNameTaken(candidateName)) { suffix += 1; - candidateName = `${snapSuggestedAccountName} ${suffix}`; + candidateName = `${baseName} ${suffix}`; } - setAccountName(candidateName); - } else { - const nextAccountName = - Engine.context.AccountsController.getNextAvailableAccountName( - KeyringTypes.snap, - ); - setAccountName(nextAccountName); - } - }, [approvalRequest, internalAccounts, checkIfNameTaken]); + return candidateName; + }, + [checkIfNameTaken], + ); + + const getInitialAccountName = useCallback( + (suggestedName?: string): string => { + if (suggestedName) { + return generateUniqueNameWithSuffix(suggestedName); + } + return Engine.context.AccountsController.getNextAvailableAccountName( + KeyringTypes.snap, + ); + }, + [generateUniqueNameWithSuffix], + ); + + useEffect(() => { + const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName; + const initialName = getInitialAccountName(suggestedName); + setAccountName(initialName); + }, [approvalRequest, getInitialAccountName]); const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, From 09ac3ca29c456df4f12512b7b984221edc24bcdd Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 15 Nov 2024 13:00:33 -0800 Subject: [PATCH 14/16] address Gutos feedback --- app/core/SnapKeyring/SnapKeyring.ts | 46 +----------------------- app/core/SnapKeyring/utils/showDialog.ts | 45 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 app/core/SnapKeyring/utils/showDialog.ts diff --git a/app/core/SnapKeyring/SnapKeyring.ts b/app/core/SnapKeyring/SnapKeyring.ts index a1666c36eaa..6d63ce6709d 100644 --- a/app/core/SnapKeyring/SnapKeyring.ts +++ b/app/core/SnapKeyring/SnapKeyring.ts @@ -2,50 +2,7 @@ import { SnapKeyring } from '@metamask/eth-snap-keyring'; import type { SnapController } from '@metamask/snaps-controllers'; import { SnapKeyringBuilderMessenger } from './types'; import Logger from '../../util/Logger'; -import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../RPCMethods/RPCMethodMiddleware'; - -interface CreateAccountConfirmationResult { - success: boolean; - name?: string; -} - -/** - * Show the account name suggestion confirmation dialog for a given Snap. - * - * @param snapId - Snap ID to show the account name suggestion dialog for. - * @param controllerMessenger - The controller messenger instance. - * @param accountNameSuggestion - Suggested name for the new account. - * @returns The user's confirmation result. - */ -export async function showAccountNameSuggestionDialog( - snapId: string, - controllerMessenger: SnapKeyringBuilderMessenger, - accountNameSuggestion: string, -): Promise { - try { - const confirmationResult = (await controllerMessenger.call( - 'ApprovalController:addRequest', - { - origin: snapId, - type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, - requestData: { - snapSuggestedAccountName: accountNameSuggestion, - }, - }, - true, - )) as CreateAccountConfirmationResult; - - if (confirmationResult) { - return { - success: confirmationResult.success, - name: confirmationResult.name, - }; - } - return { success: false }; - } catch (e) { - throw new Error(`Error occurred while showing name account dialog.\n${e}`); - } -} +import { showAccountNameSuggestionDialog } from './utils/showDialog'; /** * Constructs a SnapKeyring builder with specified handlers for managing snap accounts. @@ -93,7 +50,6 @@ export const snapKeyringBuilder = ( snapId: string, handleUserInput: (accepted: boolean) => Promise, accountNameSuggestion = '', - _displayConfirmation = false, ) => { const { id: addAccountFlowId } = controllerMessenger.call( 'ApprovalController:startFlow', diff --git a/app/core/SnapKeyring/utils/showDialog.ts b/app/core/SnapKeyring/utils/showDialog.ts new file mode 100644 index 00000000000..9358adc79d5 --- /dev/null +++ b/app/core/SnapKeyring/utils/showDialog.ts @@ -0,0 +1,45 @@ +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../RPCMethods/RPCMethodMiddleware'; +import { SnapKeyringBuilderMessenger } from '../types'; + +interface CreateAccountConfirmationResult { + success: boolean; + name?: string; +} + +/** + * Show the account name suggestion confirmation dialog for a given Snap. + * + * @param snapId - Snap ID to show the account name suggestion dialog for. + * @param controllerMessenger - The controller messenger instance. + * @param accountNameSuggestion - Suggested name for the new account. + * @returns The user's confirmation result. + */ +export async function showAccountNameSuggestionDialog( + snapId: string, + controllerMessenger: SnapKeyringBuilderMessenger, + accountNameSuggestion: string, +): Promise { + try { + const confirmationResult = (await controllerMessenger.call( + 'ApprovalController:addRequest', + { + origin: snapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: accountNameSuggestion, + }, + }, + true, + )) as CreateAccountConfirmationResult; + + if (confirmationResult) { + return { + success: confirmationResult.success, + name: confirmationResult.name, + }; + } + return { success: false }; + } catch (e) { + throw new Error(`Error occurred while showing name account dialog.\n${e}`); + } +} From 05a4231762f0c15bc8fac8dc3790afd66bfd6025 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 15 Nov 2024 13:19:01 -0800 Subject: [PATCH 15/16] simplify SnapAccountCustomName --- .../SnapAccountCustomNameApproval.tsx | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index 049c45d690e..f28e375d694 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -39,49 +39,38 @@ const SnapAccountCustomNameApproval = () => { const { styles } = useStyles(styleSheet, {}); - const onAddAccountPressed = useCallback(() => { + const onAddAccountPressed = () => { if (!isNameTaken) { onConfirm(undefined, { success: true, name: accountName }); } - }, [accountName, onConfirm, isNameTaken]); + }; - const checkIfNameTaken = useCallback( - (name: string) => - internalAccounts.some((account) => account.metadata.name === name), - [internalAccounts], - ); + const checkIfNameTaken = (name: string) => + internalAccounts.some((account) => account.metadata.name === name); + + useEffect(() => { + const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName; + const initialName = suggestedName + ? generateUniqueNameWithSuffix(suggestedName) + : Engine.context.AccountsController.getNextAvailableAccountName( + KeyringTypes.snap, + ); + setAccountName(initialName); - const generateUniqueNameWithSuffix = useCallback( - (baseName: string): string => { + function generateUniqueNameWithSuffix(baseName: string): string { let suffix = 1; let candidateName = baseName; - - while (checkIfNameTaken(candidateName)) { + while ( + internalAccounts.some( + (account) => account.metadata.name === candidateName, + ) + ) { suffix += 1; candidateName = `${baseName} ${suffix}`; } return candidateName; - }, - [checkIfNameTaken], - ); - - const getInitialAccountName = useCallback( - (suggestedName?: string): string => { - if (suggestedName) { - return generateUniqueNameWithSuffix(suggestedName); - } - return Engine.context.AccountsController.getNextAvailableAccountName( - KeyringTypes.snap, - ); - }, - [generateUniqueNameWithSuffix], - ); - - useEffect(() => { - const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName; - const initialName = getInitialAccountName(suggestedName); - setAccountName(initialName); - }, [approvalRequest, getInitialAccountName]); + } + }, [approvalRequest, internalAccounts]); const cancelButtonProps: ButtonProps = { variant: ButtonVariants.Secondary, From dd8c2faa489d6aabd49e2e0128bde69b542ec9b4 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Fri, 15 Nov 2024 13:47:30 -0800 Subject: [PATCH 16/16] fix lint --- .../SnapAccountCustomNameApproval.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx index f28e375d694..4ad320c1be8 100644 --- a/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx +++ b/app/components/Approvals/SnapAccountCustomNameApproval/SnapAccountCustomNameApproval.tsx @@ -1,5 +1,5 @@ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { TextInput, View } from 'react-native'; import ApprovalModal from '../ApprovalModal'; import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; @@ -49,19 +49,12 @@ const SnapAccountCustomNameApproval = () => { internalAccounts.some((account) => account.metadata.name === name); useEffect(() => { - const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName; - const initialName = suggestedName - ? generateUniqueNameWithSuffix(suggestedName) - : Engine.context.AccountsController.getNextAvailableAccountName( - KeyringTypes.snap, - ); - setAccountName(initialName); - function generateUniqueNameWithSuffix(baseName: string): string { let suffix = 1; let candidateName = baseName; while ( internalAccounts.some( + // eslint-disable-next-line no-loop-func (account) => account.metadata.name === candidateName, ) ) { @@ -70,6 +63,14 @@ const SnapAccountCustomNameApproval = () => { } return candidateName; } + + const suggestedName = approvalRequest?.requestData.snapSuggestedAccountName; + const initialName = suggestedName + ? generateUniqueNameWithSuffix(suggestedName) + : Engine.context.AccountsController.getNextAvailableAccountName( + KeyringTypes.snap, + ); + setAccountName(initialName); }, [approvalRequest, internalAccounts]); const cancelButtonProps: ButtonProps = {