diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 66494d89aba0..8c1ddf83979e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1482,6 +1482,12 @@ "createPassword": { "message": "Create password" }, + "createPasswordCreating": { + "message": "Creating password..." + }, + "createPasswordCreatingNote": { + "message": "This shouldn't take long" + }, "createSnapAccountDescription": { "message": "$1 wants to add a new account to MetaMask." }, @@ -4854,6 +4860,15 @@ "securityAndPrivacy": { "message": "Security & privacy" }, + "securityChangePasswordChange": { + "message": "Change password" + }, + "securityChangePasswordDescription": { + "message": "Choose a strong password to unlock MetaMask app on your device. If you lose this password, you will need your Secret Recovery Phrase to re-import your wallet." + }, + "securityChangePasswordTitle": { + "message": "Password" + }, "securityDescription": { "message": "Reduce your chances of joining unsafe networks and protect your accounts" }, @@ -4864,14 +4879,21 @@ "message": "Powered by $1", "description": "The security provider that is providing data" }, - "securitySrpBackupSrp": { - "message": "Back up Secret Recovery Phrase" - }, "securitySrpDescription": { - "message": "Back up your Secret Recovery Phrase. Be sure to store your Secret Recovery Phrase in a safe place that only you can access and won’t forget." + "message": "Back up your Secret Recovery Phrase so you don’t lose access to your wallet. Be sure to store it in a safe place that only you can access and won’t forget." }, "securitySrpWalletRecovery": { - "message": "Wallet recovery" + "message": "Reveal Secret Recovery Phrase" + }, + "securityLoginWithSocial": { + "message": "Login with $1", + "description": "The $1 is the text 'Google' or 'Apple'" + }, + "securityLoginWithSrpBackedUp": { + "message": "Secret Recovery Phrase backed up" + }, + "securityLoginWithSrpNotBackedUp": { + "message": "Back up Secret Recovery Phrase" }, "seeAllPermissions": { "message": "See all permissions", @@ -5637,6 +5659,12 @@ "message": "$1 account", "description": "$1 is the number of accounts in the list, it is either 1 or 0" }, + "srpListStateBackedUp": { + "message": "Reveal" + }, + "srpListStateNotBackedUp": { + "message": "Backup" + }, "srpPasteFailedTooManyWords": { "message": "Paste failed because it contained over 24 words. A secret recovery phrase can have a maximum of 24 words.", "description": "Description of SRP paste error when the pasted content has too many words" @@ -6643,6 +6671,9 @@ "unlock": { "message": "Unlock" }, + "unlockPageIncorrectPassword": { + "message": "Password is incorrect. Please try again." + }, "unpin": { "message": "Unpin" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 66494d89aba0..8c1ddf83979e 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1482,6 +1482,12 @@ "createPassword": { "message": "Create password" }, + "createPasswordCreating": { + "message": "Creating password..." + }, + "createPasswordCreatingNote": { + "message": "This shouldn't take long" + }, "createSnapAccountDescription": { "message": "$1 wants to add a new account to MetaMask." }, @@ -4854,6 +4860,15 @@ "securityAndPrivacy": { "message": "Security & privacy" }, + "securityChangePasswordChange": { + "message": "Change password" + }, + "securityChangePasswordDescription": { + "message": "Choose a strong password to unlock MetaMask app on your device. If you lose this password, you will need your Secret Recovery Phrase to re-import your wallet." + }, + "securityChangePasswordTitle": { + "message": "Password" + }, "securityDescription": { "message": "Reduce your chances of joining unsafe networks and protect your accounts" }, @@ -4864,14 +4879,21 @@ "message": "Powered by $1", "description": "The security provider that is providing data" }, - "securitySrpBackupSrp": { - "message": "Back up Secret Recovery Phrase" - }, "securitySrpDescription": { - "message": "Back up your Secret Recovery Phrase. Be sure to store your Secret Recovery Phrase in a safe place that only you can access and won’t forget." + "message": "Back up your Secret Recovery Phrase so you don’t lose access to your wallet. Be sure to store it in a safe place that only you can access and won’t forget." }, "securitySrpWalletRecovery": { - "message": "Wallet recovery" + "message": "Reveal Secret Recovery Phrase" + }, + "securityLoginWithSocial": { + "message": "Login with $1", + "description": "The $1 is the text 'Google' or 'Apple'" + }, + "securityLoginWithSrpBackedUp": { + "message": "Secret Recovery Phrase backed up" + }, + "securityLoginWithSrpNotBackedUp": { + "message": "Back up Secret Recovery Phrase" }, "seeAllPermissions": { "message": "See all permissions", @@ -5637,6 +5659,12 @@ "message": "$1 account", "description": "$1 is the number of accounts in the list, it is either 1 or 0" }, + "srpListStateBackedUp": { + "message": "Reveal" + }, + "srpListStateNotBackedUp": { + "message": "Backup" + }, "srpPasteFailedTooManyWords": { "message": "Paste failed because it contained over 24 words. A secret recovery phrase can have a maximum of 24 words.", "description": "Description of SRP paste error when the pasted content has too many words" @@ -6643,6 +6671,9 @@ "unlock": { "message": "Unlock" }, + "unlockPageIncorrectPassword": { + "message": "Password is incorrect. Please try again." + }, "unpin": { "message": "Unpin" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0ae2658f97e0..982f44e3c674 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -245,6 +245,7 @@ import { BRIDGE_API_BASE_URL } from '../../shared/constants/bridge'; import { MultichainWalletSnapClient } from '../../shared/lib/accounts'; import { SOLANA_WALLET_SNAP_ID } from '../../shared/lib/accounts/solana-wallet-snap'; ///: END:ONLY_INCLUDE_IF +import { FirstTimeFlowType } from '../../shared/constants/onboarding'; import { createTransactionEventFragmentWithTxId } from './lib/transaction/metrics'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { keyringSnapPermissionsBuilder } from './lib/snap-keyring/keyring-snaps-permissions'; @@ -3507,6 +3508,7 @@ export default class MetamaskController extends EventEmitter { createSeedPhraseBackup: this.createSeedPhraseBackup.bind(this), fetchAllSeedPhrases: this.fetchAllSeedPhrases.bind(this), updateBackupMetadataState: this.updateBackupMetadataState.bind(this), + changePassword: this.changePassword.bind(this), // hardware wallets connectHardware: this.connectHardware.bind(this), @@ -4704,6 +4706,33 @@ export default class MetamaskController extends EventEmitter { ); } + /** + * Changes the password of the current wallet. + * + * If the wallet is created with social login, the password is changed for the seedless onboarding flow and sync across the devices too. + * + * @param {string} newPassword - The new password. + * @param {string} oldPassword - The old password. + * @returns {Promise} + */ + async changePassword(newPassword, oldPassword) { + const { firstTimeFlowType } = this.onboardingController.state; + + if ( + firstTimeFlowType === FirstTimeFlowType.socialCreate || + firstTimeFlowType === FirstTimeFlowType.socialImport + ) { + // change password for the social login flow + await this.seedlessOnboardingController.changePassword( + newPassword, + oldPassword, + ); + } + + // also update the vault password for keyring controller + await this.keyringController.changePassword(newPassword); + } + //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 5c45b3526857..de9eea85cb60 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -59,6 +59,7 @@ import { EndowmentTypes, RestrictedEthMethods, } from '../../shared/constants/permissions'; +import { FirstTimeFlowType } from '../../shared/constants/onboarding'; import { deferredPromise } from './lib/util'; import { METAMASK_COOKIE_HANDLER } from './constants/stream'; import MetaMaskController from './metamask-controller'; @@ -799,6 +800,52 @@ describe('MetaMaskController', () => { }); }); + describe('#changePassword', () => { + it('should change the password for both seedless onboarding and keyring controller', async () => { + const oldPassword = 'old-password'; + const newPassword = 'new-password'; + + metamaskController.onboardingController.setFirstTimeFlowType( + FirstTimeFlowType.socialCreate, + ); + + await metamaskController.createNewVaultAndKeychain(oldPassword); + + const changePwdSeedlessOnboardingSpy = jest + .spyOn( + metamaskController.seedlessOnboardingController, + 'changePassword', + ) + .mockResolvedValueOnce(); + const changePwdKeyringControllerSpy = jest + .spyOn(metamaskController.keyringController, 'changePassword') + .mockResolvedValueOnce(); + + await metamaskController.changePassword(newPassword, oldPassword); + + expect(changePwdSeedlessOnboardingSpy).toHaveBeenCalledWith( + newPassword, + oldPassword, + ); + expect(changePwdKeyringControllerSpy).toHaveBeenCalledWith(newPassword); + }); + + it('should change the password for Keyring Vault for the SRP flow', async () => { + const oldPassword = 'old-password'; + const newPassword = 'new-password'; + + await metamaskController.createNewVaultAndKeychain(oldPassword); + + const changePwdKeyringControllerSpy = jest + .spyOn(metamaskController.keyringController, 'changePassword') + .mockResolvedValueOnce(); + + await metamaskController.changePassword(newPassword, oldPassword); + + expect(changePwdKeyringControllerSpy).toHaveBeenCalledWith(newPassword); + }); + }); + describe('#createNewVaultAndRestore', () => { it('should be able to call newVaultAndRestore despite a mistake.', async () => { const password = 'what-what-what'; diff --git a/test/env.js b/test/env.js index f579d7f53a33..7199431596c8 100644 --- a/test/env.js +++ b/test/env.js @@ -16,3 +16,4 @@ process.env.PUSH_NOTIFICATIONS_SERVICE_URL = process.env.PORTFOLIO_URL = 'https://portfolio.test'; process.env.METAMASK_VERSION = 'MOCK_VERSION'; process.env.TZ = 'UTC'; +process.env.WEB3AUTH_NETWORK = 'sapphire_devnet'; diff --git a/ui/components/multichain/multi-srp/srp-list/srp-list.tsx b/ui/components/multichain/multi-srp/srp-list/srp-list.tsx index 280e2ae4cc4a..2cf271404b66 100644 --- a/ui/components/multichain/multi-srp/srp-list/srp-list.tsx +++ b/ui/components/multichain/multi-srp/srp-list/srp-list.tsx @@ -16,6 +16,7 @@ import { AlignItems, BlockSize, TextVariant, + IconColor, } from '../../../../helpers/constants/design-system'; import { getMetaMaskAccounts } from '../../../../selectors/selectors'; import { InternalAccountWithBalance } from '../../../../selectors/selectors.types'; @@ -52,6 +53,8 @@ export const SrpList = ({ showAccountsInitState, ); + const srpListStateBackedUp = true; + const showHideText = (index: number, numberOfAccounts: number): string => { if (numberOfAccounts > 1) { return showAccounts[index] @@ -89,7 +92,9 @@ export const SrpList = ({ justifyContent={JustifyContent.spaceBetween} > - {t('srpListName', [index + 1])} + + {t('srpListName', [index + 1])} + {!hideShowAccounts && ( )} - + + + {srpListStateBackedUp + ? t('srpListStateBackedUp') + : t('srpListStateNotBackedUp')} + + + {showAccounts[index] && ( diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index dfa68149a999..6acbcd989d7f 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -71,6 +71,10 @@ export const REVEAL_SRP_LIST_ROUTE = PATH_NAME_MAP[REVEAL_SRP_LIST_ROUTE] = 'Reveal Secret Recovery Phrase List Page'; +export const SECURITY_PASSWORD_CHANGE_ROUTE = + '/settings/security-and-privacy/password-change'; +PATH_NAME_MAP[SECURITY_PASSWORD_CHANGE_ROUTE] = 'Change Password'; + export const BACKUPANDSYNC_ROUTE = '/settings/security-and-privacy/backup-and-sync'; PATH_NAME_MAP[BACKUPANDSYNC_ROUTE] = 'Backup And Sync Settings Page'; diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index c16088ac1b6e..a8ea48e162a3 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -5,6 +5,7 @@ @import 'networks-tab/index'; @import 'settings-tab/index'; @import 'contact-list-tab/index'; +@import 'security-tab/index'; .settings-page { position: relative; diff --git a/ui/pages/settings/security-tab/change-password/change-password.stories.tsx b/ui/pages/settings/security-tab/change-password/change-password.stories.tsx new file mode 100644 index 000000000000..21cd6cb14457 --- /dev/null +++ b/ui/pages/settings/security-tab/change-password/change-password.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ChangePassword from './change-password'; + +export default { + title: 'Pages/SettingsPage/ChangePassword', +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/settings/security-tab/change-password/change-password.tsx b/ui/pages/settings/security-tab/change-password/change-password.tsx new file mode 100644 index 000000000000..0ffe71845e3c --- /dev/null +++ b/ui/pages/settings/security-tab/change-password/change-password.tsx @@ -0,0 +1,178 @@ +import EventEmitter from 'events'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { + Box, + Button, + FormTextField, + Text, + TextFieldType, +} from '../../../../components/component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextColor, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { isBeta, isFlask } from '../../../../helpers/utils/build-types'; +import Mascot from '../../../../components/ui/mascot'; +import Spinner from '../../../../components/ui/spinner'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { changePassword, verifyPassword } from '../../../../store/actions'; +import PasswordForm from '../../../../components/app/password-form/password-form'; + +const ChangePasswordSteps = { + VerifyCurrentPassword: 1, + ChangePassword: 2, + CreatingPassword: 3, +}; + +const ChangePassword = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const history = useHistory(); + const [eventEmitter] = useState(new EventEmitter()); + const [step, setStep] = useState(ChangePasswordSteps.VerifyCurrentPassword); + + const [currentPassword, setCurrentPassword] = useState(''); + const [isIncorrectPasswordError, setIsIncorrectPasswordError] = + useState(false); + + const [newPassword, setNewPassword] = useState(''); + + const renderMascot = () => { + if (isFlask()) { + return ( + + ); + } + if (isBeta()) { + return ( + + ); + } + return ( + + ); + }; + + const handleSubmitCurrentPassword = async () => { + try { + await verifyPassword(currentPassword); + setIsIncorrectPasswordError(false); + setStep(ChangePasswordSteps.ChangePassword); + } catch (error) { + setIsIncorrectPasswordError(true); + } + }; + + const handleSubmitNewPassword = async () => { + if (!newPassword) { + return; + } + + try { + setStep(ChangePasswordSteps.CreatingPassword); + await dispatch(changePassword(newPassword, currentPassword)); + + // upon successful password change, go back to the settings page + history.goBack(); + } catch (error) { + console.error(error); + setStep(ChangePasswordSteps.VerifyCurrentPassword); + } + }; + + return ( + + {step === ChangePasswordSteps.VerifyCurrentPassword && ( + { + e.preventDefault(); + handleSubmitCurrentPassword(); + }} + > + { + setCurrentPassword(e.target.value); + setIsIncorrectPasswordError(false); + }} + /> + + + + )} + + {step === ChangePasswordSteps.ChangePassword && ( + { + e.preventDefault(); + handleSubmitNewPassword(); + }} + > + + setNewPassword(password)} /> + + + + )} + + {step === ChangePasswordSteps.CreatingPassword && ( + +
{renderMascot()}
+ + + {t('createPasswordCreating')} + + + {t('createPasswordCreatingNote')} + +
+ )} +
+ ); +}; + +export default ChangePassword; diff --git a/ui/pages/settings/security-tab/change-password/index.ts b/ui/pages/settings/security-tab/change-password/index.ts new file mode 100644 index 000000000000..c27fed54b984 --- /dev/null +++ b/ui/pages/settings/security-tab/change-password/index.ts @@ -0,0 +1 @@ +export { default } from './change-password'; diff --git a/ui/pages/settings/security-tab/index.scss b/ui/pages/settings/security-tab/index.scss new file mode 100644 index 000000000000..f77bf75e9fa2 --- /dev/null +++ b/ui/pages/settings/security-tab/index.scss @@ -0,0 +1,40 @@ +@use "design-system"; + +.protect-wallet { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + + &__container { + border-radius: 0.5rem; + background-color: var(--color-background-muted); + } + + &__container-body { + border-top: 1px solid var(--color-background-default); + } +} + +.change-password { + &__spinner { + width: 1.5rem; + height: 1.5rem; + margin-bottom: 1rem; + } + + @include design-system.screen-sm-max { + height: 100%; + } +} + +.srp-reveal-list { + &__divider { + height: 1px; + background-color: var(--color-border-muted); + } + + &__social-login-card { + cursor: pointer; + } +} diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index d8ee42eec52f..6392fbf55fcc 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -32,6 +32,8 @@ import { Box, Text, ButtonSize, + BannerAlert, + BannerAlertSeverity, } from '../../../components/component-library'; import TextField from '../../../components/ui/text-field'; import ToggleButton from '../../../components/ui/toggle-button'; @@ -49,6 +51,7 @@ import { import { ADD_POPULAR_CUSTOM_NETWORK, REVEAL_SRP_LIST_ROUTE, + SECURITY_PASSWORD_CHANGE_ROUTE, } from '../../../helpers/constants/routes'; import { getNumberOfSettingRoutesInTab, @@ -106,6 +109,9 @@ export default class SecurityTab extends PureComponent { metaMetricsDataDeletionId: PropTypes.string, hdEntropyIndex: PropTypes.number, hasMultipleHdKeyrings: PropTypes.bool, + socialLoginEnabled: PropTypes.bool, + socialLoginType: PropTypes.string, + seedPhraseBackedUp: PropTypes.bool, }; state = { @@ -166,7 +172,13 @@ export default class SecurityTab extends PureComponent { renderSeedWords() { const { t } = this.context; - const { history, hasMultipleHdKeyrings } = this.props; + const { + history, + hasMultipleHdKeyrings, + seedPhraseBackedUp, + socialLoginEnabled, + socialLoginType, + } = this.props; return ( <> @@ -180,6 +192,31 @@ export default class SecurityTab extends PureComponent {
{t('securitySrpDescription')}
+ {socialLoginEnabled ? ( + + ) : ( + + )} + + + ); + } + renderSecurityAlertsToggle() { const { t } = this.context; const { securityAlertsEnabled } = this.props; @@ -1165,6 +1233,7 @@ export default class SecurityTab extends PureComponent { {this.context.t('security')} {this.renderSeedWords()} + {this.renderChangePassword()} {this.renderSecurityAlertsToggle()} {this.context.t('privacy')} diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index d748783b6d75..7bf4b3121a61 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -25,10 +25,13 @@ import { getIsSecurityAlertsEnabled, getMetaMetricsDataDeletionId, getHDEntropyIndex, -} from '../../../selectors/selectors'; + getMetaMaskHdKeyrings, + getSocialLoginType, + isSocialLoginFlow, +} from '../../../selectors'; import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; -import { getMetaMaskHdKeyrings } from '../../../selectors'; +import { getSeedPhraseBackedUp } from '../../../ducks/metamask/metamask'; import SecurityTab from './security-tab.component'; const mapStateToProps = (state) => { @@ -76,6 +79,9 @@ const mapStateToProps = (state) => { metaMetricsDataDeletionId: getMetaMetricsDataDeletionId(state), hdEntropyIndex: getHDEntropyIndex(state), hasMultipleHdKeyrings, + socialLoginEnabled: isSocialLoginFlow(state), + socialLoginType: getSocialLoginType(state), + seedPhraseBackedUp: getSeedPhraseBackedUp(state), }; }; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index c31dbc90e30d..ee3c8dbde1b1 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -24,6 +24,7 @@ import { SNAP_SETTINGS_ROUTE, REVEAL_SRP_LIST_ROUTE, BACKUPANDSYNC_ROUTE, + SECURITY_PASSWORD_CHANGE_ROUTE, } from '../../helpers/constants/routes'; import { getSettingsRoutes } from '../../helpers/utils/settings-search'; @@ -53,6 +54,8 @@ import { SnapSettingsRenderer } from '../../components/app/snaps/snap-settings-p import SettingsTab from './settings-tab'; import AdvancedTab from './advanced-tab'; import InfoTab from './info-tab'; +// TODO: Fix this eslint error +// eslint-disable-next-line import/default import SecurityTab from './security-tab'; import ContactListTab from './contact-list-tab'; import DeveloperOptionsTab from './developer-options-tab'; @@ -61,6 +64,7 @@ import SettingsSearch from './settings-search'; import SettingsSearchList from './settings-search-list'; import { RevealSrpList } from './security-tab/reveal-srp-list'; import BackupAndSyncTab from './backup-and-sync-tab'; +import ChangePassword from './security-tab/change-password'; class SettingsPage extends PureComponent { static propTypes = { @@ -452,8 +456,10 @@ class SettingsPage extends PureComponent { return ; }} /> + + {(process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS || process.env.IN_TEST) && ( + ( { const isAddContactPage = Boolean(pathname.match(CONTACT_ADD_ROUTE)); const isEditContactPage = Boolean(pathname.match(CONTACT_EDIT_ROUTE)); const isRevealSrpListPage = Boolean(pathname.match(REVEAL_SRP_LIST_ROUTE)); + const isPasswordChangePage = Boolean( + pathname.match(SECURITY_PASSWORD_CHANGE_ROUTE), + ); const isNetworksFormPage = Boolean(pathname.match(NETWORKS_FORM_ROUTE)) || Boolean(pathname.match(ADD_NETWORK_ROUTE)); @@ -99,7 +102,7 @@ const mapStateToProps = (state, ownProps) => { backRoute = NETWORKS_ROUTE; } else if (isAddPopularCustomNetwork) { backRoute = NETWORKS_ROUTE; - } else if (isRevealSrpListPage) { + } else if (isRevealSrpListPage || isPasswordChangePage) { backRoute = SECURITY_ROUTE; } diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index e56f679eda5b..db25c1a5c50f 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -12,6 +12,7 @@ import { GENERAL_ROUTE, NETWORKS_FORM_ROUTE, NETWORKS_ROUTE, + SECURITY_PASSWORD_CHANGE_ROUTE, SECURITY_ROUTE, SETTINGS_ROUTE, } from '../../helpers/constants/routes'; @@ -39,6 +40,7 @@ const ROUTES_TO_I18N_KEYS = { [GENERAL_ROUTE]: 'general', [NETWORKS_FORM_ROUTE]: 'networks', [NETWORKS_ROUTE]: 'networks', + [SECURITY_PASSWORD_CHANGE_ROUTE]: 'securityPassword', [SECURITY_ROUTE]: 'securityAndPrivacy', }; diff --git a/ui/selectors/social-sync.ts b/ui/selectors/social-sync.ts index 6a83cd7921aa..31da09c8d288 100644 --- a/ui/selectors/social-sync.ts +++ b/ui/selectors/social-sync.ts @@ -1,5 +1,8 @@ import { KeyringControllerState } from '@metamask/keyring-controller'; -import { AuthConnection, SeedlessOnboardingControllerState } from '@metamask/seedless-onboarding-controller'; +import { + AuthConnection, + SeedlessOnboardingControllerState, +} from '@metamask/seedless-onboarding-controller'; export type BackupState = { metamask: KeyringControllerState & SeedlessOnboardingControllerState; diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index bfd83a79ae5c..e9ac507dbc85 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -253,7 +253,9 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, ]; - await store.dispatch(actions.restoreSocialBackupAndGetSeedPhrase('password')); + await store.dispatch( + actions.restoreSocialBackupAndGetSeedPhrase('password'), + ); expect(fetchAllSeedPhrasesStub.callCount).toStrictEqual(1); expect(createNewVaultAndRestoreStub.callCount).toStrictEqual(1); @@ -284,7 +286,9 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, ]; - await store.dispatch(actions.restoreSocialBackupAndGetSeedPhrase('password')); + await store.dispatch( + actions.restoreSocialBackupAndGetSeedPhrase('password'), + ); expect(fetchAllSeedPhrasesStub.callCount).toStrictEqual(1); expect(createNewVaultAndRestoreStub.callCount).toStrictEqual(0); @@ -314,6 +318,30 @@ describe('Actions', () => { }); }); + describe('#changePassword', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should change the password for both seedless onboarding and keyring controller', async () => { + const store = mockStore(); + const oldPassword = 'old-password'; + const newPassword = 'new-password'; + + const changePasswordStub = background.changePassword.callsFake( + (_, __, cb) => cb(), + ); + + setBackgroundConnection(background); + + await store.dispatch(actions.changePassword(newPassword, oldPassword)); + + expect( + changePasswordStub.calledOnceWith(newPassword, oldPassword), + ).toStrictEqual(true); + }); + }); + describe('#tryUnlockMetamask', () => { afterEach(() => { sinon.restore(); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 67bdca3890f1..8330a8e6ea39 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -310,6 +310,35 @@ export function restoreSocialBackupAndGetSeedPhrase( }; } +/** + * Changes the password of the currently unlocked account. + * + * This function changes the password of the currently unlocked account (Keyring Vault) and + * also change the wallet password of the social login account. + * + * This changes affects the multiple devices sync, i.e. users will have to unlock the account + * using new password on any other devices where the account is unlocked. + * + * @param newPassword - The new password. + * @param oldPassword - The old password. + */ +export function changePassword( + newPassword: string, + oldPassword: string, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + try { + await submitRequestToBackground('changePassword', [ + newPassword, + oldPassword, + ]); + } catch (error) { + dispatch(displayWarning(error)); + throw error; + } + }; +} + export function tryUnlockMetamask( password: string, ): ThunkAction {