From 16a0650b6645dff46252a29daebe01c21c098a73 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 12 Mar 2024 12:07:54 +0100 Subject: [PATCH] Feat/lw 9709 prompt password when unlocking account (#901) * fix(ui): password box LW-9709 The PasswordBox component layout in lace/ui wasn't responsive and broke in different width scenarios. This commit fixes the layout issues so that the password box can have any size. * feat(core): enable account password prompt LW-9709 This is the basis of the core component which will later be wired-up in Lace to prompt for the wallet password when enabling an account. * feat(extension): wire-up password prompt to enable account LW-9709 extracted the password prompting out of the wallet manager into the UI and optionally pass it down instead. * feat(core): add ui for enabling account HW flow LW-9709 * feat(extension): integrate ui for enabling account HW in Lace LW-9709 * fix(extension): improve unit test for adding accounts LW-9709 --- .../DropdownMenuOverlay.tsx | 10 +- .../components/WalletAccounts.tsx | 178 +++++++++++++++--- .../hooks/__tests__/useWalletManager.test.tsx | 9 +- .../src/hooks/useWalletManager.ts | 10 +- .../src/lib/translations/en.json | 24 +++ .../ui/assets/icons/exclamation-circle.svg | 3 + .../src/ui/assets/images/hardware-wallet.svg | 19 ++ .../src/ui/assets/images/lace-portal-01.svg | 89 +++++++++ .../EnableAccountConfirmWithHW.module.scss | 6 + .../EnableAccountConfirmWithHW.stories.tsx | 55 ++++++ .../EnableAccountConfirmWithHW.tsx | 99 ++++++++++ .../EnableAccountConfirmWithHW/index.ts | 1 + .../EnableAccountPasswordPrompt.module.scss | 16 ++ .../EnableAccountPasswordPrompt.stories.ts | 52 +++++ .../EnableAccountPasswordPrompt.tsx | 88 +++++++++ .../EnableAccountPasswordPrompt/index.ts | 1 + .../core/src/ui/components/Account/hooks.ts | 23 --- .../core/src/ui/components/Account/index.ts | 3 +- packages/core/src/ui/hooks/index.ts | 1 + .../core/src/ui/hooks/useDialogWithData.ts | 27 +++ .../buttons/call-to-action-button.tsx | 2 +- .../password-box-button.component.tsx | 4 +- .../password-box/password-box-button.css.ts | 1 - .../password-box-input.component.tsx | 42 ++--- .../password-box/password-box-input.css.ts | 26 ++- .../password-box/password-box.stories.tsx | 3 +- 26 files changed, 697 insertions(+), 95 deletions(-) create mode 100644 packages/core/src/ui/assets/icons/exclamation-circle.svg create mode 100644 packages/core/src/ui/assets/images/hardware-wallet.svg create mode 100644 packages/core/src/ui/assets/images/lace-portal-01.svg create mode 100644 packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.module.scss create mode 100644 packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.stories.tsx create mode 100644 packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.tsx create mode 100644 packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/index.ts create mode 100644 packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.module.scss create mode 100644 packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.stories.ts create mode 100644 packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.tsx create mode 100644 packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/index.ts delete mode 100644 packages/core/src/ui/components/Account/hooks.ts create mode 100644 packages/core/src/ui/hooks/useDialogWithData.ts diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx index 588b9f3f2..932f282eb 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/DropdownMenuOverlay.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, VFC } from 'react'; +import React, { ReactNode, useCallback, useState, VFC } from 'react'; import { Menu, MenuProps } from 'antd'; import { AddNewWalletLink, @@ -49,6 +49,8 @@ export const DropdownMenuOverlay: VFC = ({ sendAnalyticsEvent(PostHogAction.UserWalletProfileNetworkClick); }; + const goBackToMainSection = useCallback(() => setCurrentSection(Sections.Main), []); + topSection = topSection ?? ; return ( @@ -77,10 +79,8 @@ export const DropdownMenuOverlay: VFC = ({ )} - {currentSection === Sections.NetworkInfo && setCurrentSection(Sections.Main)} />} - {currentSection === Sections.WalletAccounts && ( - setCurrentSection(Sections.Main)} isPopup={isPopup} /> - )} + {currentSection === Sections.NetworkInfo && } + {currentSection === Sections.WalletAccounts && } ); }; diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx index c86b612ba..332cff01e 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/WalletAccounts.tsx @@ -5,7 +5,14 @@ import { NavigationButton, toast } from '@lace/common'; import styles from './WalletAccounts.module.scss'; import { ProfileDropdown } from '@lace/ui'; import { AccountData } from '@lace/ui/dist/design-system/profile-dropdown/accounts/profile-dropdown-accounts-list.component'; -import { DisableAccountConfirmation, EditAccountDrawer, useAccountDataModal } from '@lace/core'; +import { + DisableAccountConfirmation, + EditAccountDrawer, + EnableAccountConfirmWithHW, + EnableAccountConfirmWithHWState, + EnableAccountPasswordPrompt, + useDialogWithData +} from '@lace/core'; import { useWalletStore } from '@src/stores'; import { useWalletManager } from '@hooks'; import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution'; @@ -17,6 +24,17 @@ import { BrowserViewSections } from '@lib/scripts/types'; const defaultAccountName = (accountNumber: number) => `Account #${accountNumber}`; const NUMBER_OF_ACCOUNTS_PER_WALLET = 24; +const HW_CONNECT_TIMEOUT_MS = 30_000; + +type EnableAccountPasswordDialogData = { + accountIndex: number; + wasPasswordIncorrect?: boolean; +}; + +type EnableAccountHWSigningDialogData = { + accountIndex: number; + state: EnableAccountConfirmWithHWState; +}; export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: () => void }): React.ReactElement => { const { t } = useTranslation(); @@ -27,17 +45,21 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: }), [t] ); - const editAccountDrawer = useAccountDataModal(); const backgroundServices = useBackgroundServiceAPIContext(); - const disableAccountConfirmation = useAccountDataModal(); const { manageAccountsWallet: wallet, cardanoWallet, setIsDropdownMenuOpen } = useWalletStore(); + const { walletRepository, addAccount, activateWallet } = useWalletManager(); const { source: { wallet: { walletId: activeWalletId }, account: activeAccount } } = cardanoWallet; - const { walletRepository, addAccount, activateWallet } = useWalletManager(); + + const editAccountDrawer = useDialogWithData(); + const disableAccountConfirmation = useDialogWithData(); + const enableAccountPasswordDialog = useDialogWithData(); + const enableAccountHWSigningDialog = useDialogWithData(); + const disableUnlock = useMemo( () => isPopup && @@ -72,20 +94,27 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: [wallet, activeAccount?.accountIndex, activeWalletId, disableUnlock] ); + const closeDropdownAndShowAccountActivated = useCallback( + (accountName: string) => { + setIsDropdownMenuOpen(false); + toast.notify({ + duration: TOAST_DEFAULT_DURATION, + text: t('multiWallet.activated.account', { accountName }) + }); + }, + [setIsDropdownMenuOpen, t] + ); + const activateAccount = useCallback( async (accountIndex: number) => { await activateWallet({ walletId: wallet.walletId, accountIndex }); - setIsDropdownMenuOpen(false); const accountName = accountsData.find((acc) => acc.accountNumber === accountIndex)?.label; - toast.notify({ - duration: TOAST_DEFAULT_DURATION, - text: t('multiWallet.activated.account', { accountName }) - }); + closeDropdownAndShowAccountActivated(accountName); }, - [wallet.walletId, activateWallet, accountsData, setIsDropdownMenuOpen, t] + [wallet.walletId, activateWallet, accountsData, closeDropdownAndShowAccountActivated] ); const editAccount = useCallback( @@ -100,27 +129,77 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: [disableAccountConfirmation, accountsData] ); + const showHWErrorState = useCallback(() => { + enableAccountHWSigningDialog.setData({ + ...enableAccountHWSigningDialog.data, + state: 'error' + }); + }, [enableAccountHWSigningDialog]); + + const unlockHWAccount = useCallback( + async (accountIndex: number) => { + const name = defaultAccountName(accountIndex); + try { + const timeout = setTimeout(showHWErrorState, HW_CONNECT_TIMEOUT_MS); + await addAccount({ + wallet, + accountIndex, + metadata: { name } + }); + clearTimeout(timeout); + enableAccountHWSigningDialog.hide(); + closeDropdownAndShowAccountActivated(name); + } catch { + showHWErrorState(); + } + }, + [addAccount, wallet, enableAccountHWSigningDialog, closeDropdownAndShowAccountActivated, showHWErrorState] + ); + const unlockAccount = useCallback( async (accountIndex: number) => { + switch (wallet.type) { + case WalletType.InMemory: + enableAccountPasswordDialog.setData({ accountIndex }); + enableAccountPasswordDialog.open(); + break; + case WalletType.Ledger: + case WalletType.Trezor: + enableAccountHWSigningDialog.setData({ + accountIndex, + state: 'signing' + }); + enableAccountHWSigningDialog.open(); + await unlockHWAccount(accountIndex); + } + }, + [wallet.type, enableAccountPasswordDialog, enableAccountHWSigningDialog, unlockHWAccount] + ); + + const unlockInMemoryWalletAccountWithPassword = useCallback( + async (passphrase: Uint8Array) => { + const { accountIndex } = enableAccountPasswordDialog.data; const name = defaultAccountName(accountIndex); - await addAccount({ - wallet, - accountIndex, - metadata: { name } - }); - setIsDropdownMenuOpen(false); - toast.notify({ - duration: TOAST_DEFAULT_DURATION, - text: t('multiWallet.activated.account', { accountName: name }) - }); + try { + await addAccount({ + wallet, + accountIndex, + passphrase, + metadata: { name: defaultAccountName(accountIndex) } + }); + enableAccountPasswordDialog.hide(); + closeDropdownAndShowAccountActivated(name); + } catch { + enableAccountPasswordDialog.setData({ ...enableAccountPasswordDialog.data, wasPasswordIncorrect: true }); + } }, - [wallet, addAccount, t, setIsDropdownMenuOpen] + [wallet, addAccount, enableAccountPasswordDialog, closeDropdownAndShowAccountActivated] ); const lockAccount = useCallback(async () => { await walletRepository.removeAccount({ walletId: wallet.walletId, - accountIndex: disableAccountConfirmation.accountData.accountNumber + accountIndex: disableAccountConfirmation.data.accountNumber }); disableAccountConfirmation.hide(); @@ -130,7 +209,7 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: async (newAccountName: string) => { await walletRepository.updateAccountMetadata({ walletId: wallet.walletId, - accountIndex: editAccountDrawer.accountData.accountNumber, + accountIndex: editAccountDrawer.data.accountNumber, metadata: { name: newAccountName } }); editAccountDrawer.hide(); @@ -166,12 +245,61 @@ export const WalletAccounts = ({ isPopup, onBack }: { isPopup: boolean; onBack: /> + {/* Conditionally render the password prompt to make sure + the password is not stored in the component state */} + {enableAccountPasswordDialog.isOpen && ( + + )} + {enableAccountHWSigningDialog.isOpen && ( + { + enableAccountHWSigningDialog.setData({ + ...enableAccountHWSigningDialog.data, + state: 'signing' + }); + unlockHWAccount(enableAccountHWSigningDialog.data?.accountIndex); + }} + state={enableAccountHWSigningDialog.data?.state} + translations={{ + title: t('account.enable.title'), + headline: t('account.enable.hw.headline'), + errorHeadline: t('account.enable.hw.errorHeadline'), + description: t('account.enable.hw.description'), + errorDescription: t('account.enable.hw.errorDescription'), + errorHelpLink: t('account.enable.hw.errorHelpLink'), + buttons: { + cancel: t('account.enable.hw.buttons.cancel'), + waiting: t('account.enable.hw.buttons.waiting', { device: wallet.type }), + signing: t('account.enable.hw.buttons.signing'), + error: t('account.enable.hw.buttons.tryAgain') + } + }} + /> + )} { Ledger: { LedgerKeyAgent: { createWithDevice: mockLedgerCreateWithDevice, + checkDeviceConnection: mockLedgerCheckDeviceConnection, getXpub: mockLedgerGetXpub } }, @@ -685,8 +687,8 @@ describe('Testing useWalletManager hook', () => { '8403cf9d8267a7169381dd476f4fda48e1926fec8942ec51892e428e152fbed4835711cccb7efcae379627f477abb46c883f6b0c221f3aea40f9d931d2e8fdc69f85f16eb91ca380fc2e1edc2543e4dd71c1866208ea6c6960bca99f974e25776067e9a242b0e4066b96bd4d89ca99db5bd77bb65573b9cbeef85222ceed6d5a4dc516213ace986f03b183365505119b9a0abdc4375bfdf2363d7433' } }, + passphrase: Buffer.from('passphrase1'), prepare: () => { - global.prompt = jest.fn(() => 'passphrase1'); mockEmip3decrypt.mockImplementationOnce( jest.requireActual('@lace/cardano').Wallet.KeyManagement.emip3decrypt ); @@ -708,13 +710,14 @@ describe('Testing useWalletManager hook', () => { it.each(walletTypes)( 'derives extended account public key for $type wallet and adds new account into the repository', - async ({ type, walletProps, prepare }) => { + async ({ type, walletProps, prepare, passphrase }) => { prepare(); const walletId = 'bip32-wallet-id'; const addAccountProps = { wallet: { walletId, type, ...walletProps } as AnyBip32Wallet, accountIndex: 0, - metadata: { name: 'new account' } + metadata: { name: 'new account' }, + passphrase }; const { addAccount } = render(); diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index d567b35ae..a9bf19990 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -61,6 +61,7 @@ type WalletManagerAddAccountProps = { wallet: AnyBip32Wallet; metadata: Wallet.AccountMetadata; accountIndex: number; + passphrase?: Uint8Array; }; type ActivateWalletProps = Omit; @@ -101,6 +102,7 @@ const getHwExtendedAccountPublicKey = async ( ) => { switch (walletType) { case WalletType.Ledger: + await Wallet.Ledger.LedgerKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web); return Wallet.Ledger.LedgerKeyAgent.getXpub({ communicationType: Wallet.KeyManagement.CommunicationType.Web, deviceConnection: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined, @@ -120,13 +122,13 @@ const getHwExtendedAccountPublicKey = async ( const getExtendedAccountPublicKey = async ( wallet: AnyBip32Wallet, - accountIndex: number + accountIndex: number, + passphrase?: Uint8Array ) => { // eslint-disable-next-line sonarjs/no-small-switch switch (wallet.type) { case WalletType.InMemory: { // eslint-disable-next-line no-alert - const passphrase = Buffer.from(prompt('Please enter your passphrase')); const rootPrivateKeyBytes = await Wallet.KeyManagement.emip3decrypt( Buffer.from(wallet.encryptedSecrets.rootPrivateKeyBytes, 'hex'), passphrase @@ -705,8 +707,8 @@ export const useWalletManager = (): UseWalletManager => { ); const addAccount = useCallback( - async ({ wallet, accountIndex, metadata }: WalletManagerAddAccountProps): Promise => { - const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex); + async ({ wallet, accountIndex, metadata, passphrase }: WalletManagerAddAccountProps): Promise => { + const extendedAccountPublicKey = await getExtendedAccountPublicKey(wallet, accountIndex, passphrase); await walletRepository.addAccount({ accountIndex, extendedAccountPublicKey, diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 922d8dff2..a41de159a 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1727,6 +1727,30 @@ "footer.save": "Save", "footer.cancel": "Cancel" }, + "enable": { + "title": "Enable account", + "inMemory": { + "headline": "Confirm action", + "description": "Enter password to confirm this action.", + "passwordPlaceholder": "Password", + "wrongPassword": "Wrong Password", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "hw": { + "headline": "Confirm with Your Hardware Wallet", + "description": "Connect and unlock your device. Then, follow instructions to confirm your action.", + "errorHeadline": "Sorry! Something went wrong", + "errorDescription": "Please ensure your device is properly connected and unlocked.", + "errorHelpLink": "Having trouble?", + "buttons": { + "waiting": "Waiting for {{device}}", + "signing": "Signing in progress", + "cancel": "Cancel", + "tryAgain": "Try again" + } + } + }, "disable": { "title": "Hold up!", "description": "Are you sure you want to disable this account? You can re-enable it later", diff --git a/packages/core/src/ui/assets/icons/exclamation-circle.svg b/packages/core/src/ui/assets/icons/exclamation-circle.svg new file mode 100644 index 000000000..85439670f --- /dev/null +++ b/packages/core/src/ui/assets/icons/exclamation-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/images/hardware-wallet.svg b/packages/core/src/ui/assets/images/hardware-wallet.svg new file mode 100644 index 000000000..b02e9ca92 --- /dev/null +++ b/packages/core/src/ui/assets/images/hardware-wallet.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/ui/assets/images/lace-portal-01.svg b/packages/core/src/ui/assets/images/lace-portal-01.svg new file mode 100644 index 000000000..2f0a360c7 --- /dev/null +++ b/packages/core/src/ui/assets/images/lace-portal-01.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.module.scss b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.module.scss new file mode 100644 index 000000000..86b5becd5 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.module.scss @@ -0,0 +1,6 @@ +.text { + text-align: center; + h2 { + width: 100%; + } +} diff --git a/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.stories.tsx b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.stories.tsx new file mode 100644 index 000000000..72c13ae42 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.stories.tsx @@ -0,0 +1,55 @@ +import { ComponentProps } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { EnableAccountConfirmWithHW } from './EnableAccountConfirmWithHW'; + +const meta: Meta = { + title: 'Accounts/EnableAccountConfirmWithHW', + component: EnableAccountConfirmWithHW, + parameters: { + layout: 'centered' + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + open: true, + state: 'waiting', + onRetry: () => void 0, + onCancel: () => void 0, + isPopup: false, + translations: { + title: 'Enable account', + headline: 'Confirm with Your Hardware Wallet', + description: 'Connect and unlock your device. Then, follow instructions to confirm your action.', + errorHeadline: 'Sorry! Something went wrong', + errorDescription: 'Please ensure your device is properly connected and unlocked.', + errorHelpLink: 'Having trouble?', + buttons: { + cancel: 'Cancel', + waiting: 'Waiting for device', + signing: 'Signing in progress', + error: 'Try again' + } + } +}; + +export const Waiting: Story = { + args: data +}; + +export const Signing: Story = { + args: { + ...data, + state: 'signing' + } +}; + +export const ErrorCase: Story = { + args: { + ...data, + state: 'error' + } +}; diff --git a/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.tsx b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.tsx new file mode 100644 index 000000000..800b4f6f5 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/EnableAccountConfirmWithHW.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Box, Button, Flex, Loader, Text } from '@lace/ui'; +import { Drawer, DrawerNavigation } from '@lace/common'; +import { ReactComponent as HardwareWalletIcon } from '../../../assets/images/hardware-wallet.svg'; +import { ReactComponent as ExclamationCircle } from '../../../assets/icons/exclamation-circle.svg'; +import styles from './EnableAccountConfirmWithHW.module.scss'; + +export type EnableAccountConfirmWithHWState = 'waiting' | 'signing' | 'error'; + +interface Props { + open: boolean; + state: EnableAccountConfirmWithHWState; + onCancel: () => void; + onRetry: () => void; + isPopup: boolean; + translations: { + title: string; + headline: string; + description: string; + errorHeadline: string; + errorDescription: string; + errorHelpLink: string; + buttons: { + cancel: string; + waiting: string; + signing: string; + error: string; + }; + }; +} + +export const EnableAccountConfirmWithHW = ({ + open, + state, + isPopup, + onRetry, + onCancel, + translations +}: Props): JSX.Element => ( + } + onClose={onCancel} + popupView={isPopup} + footer={ + + + } + /> + + + + } + > + + + {state !== 'error' ? ( + + ) : ( + + )} + + {state !== 'error' ? translations.headline : translations.errorHeadline} + + + {state !== 'error' ? ( + translations.description + ) : ( + <> + {translations.errorDescription}  + + {translations.errorHelpLink} + + + )} + + + + +); diff --git a/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/index.ts b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/index.ts new file mode 100644 index 000000000..70fe3e630 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountConfirmWithHW/index.ts @@ -0,0 +1 @@ +export { EnableAccountConfirmWithHW, EnableAccountConfirmWithHWState } from './EnableAccountConfirmWithHW'; diff --git a/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.module.scss b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.module.scss new file mode 100644 index 000000000..5bcf697a5 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.module.scss @@ -0,0 +1,16 @@ +.passwordExtendedLayout { + flex: 1; + padding: 40px 58px 0 58px; // required to mimic existing layout in send tx flow +} + +.passwordPopUpLayout { + padding-top: 64px; +} + +.passwordBoxPopupContainer { + width: 100%; +} + +.description { + color: var(--text-color-secondary, #a9a9a9); +} diff --git a/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.stories.ts b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.stories.ts new file mode 100644 index 000000000..57ad553e9 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.stories.ts @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { EnableAccountPasswordPrompt } from './EnableAccountPasswordPrompt'; +import { ComponentProps } from 'react'; + +const meta: Meta = { + title: 'Accounts/EnableAccountPasswordPrompt', + component: EnableAccountPasswordPrompt, + parameters: { + layout: 'centered' + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + translations: { + title: 'Enable account', + headline: 'Confirm action', + description: 'Enter password to confirm this action.', + passwordPlaceholder: 'Password', + wrongPassword: 'Wrong password', + cancel: 'Cancel', + confirm: 'Confirm' + }, + wasPasswordIncorrect: false, + onConfirm: () => void 0, + onCancel: () => void 0, + open: true, + isPopup: false +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const PopUp: Story = { + args: { + ...data, + isPopup: true + } +}; + +export const IncorrectPassword: Story = { + args: { + ...data, + wasPasswordIncorrect: true + } +}; diff --git a/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.tsx b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.tsx new file mode 100644 index 000000000..8f7c39179 --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/EnableAccountPasswordPrompt.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { Box, Button, Flex, PasswordBox, Text } from '@lace/ui'; +import { Drawer, DrawerNavigation } from '@lace/common'; +import styles from './EnableAccountPasswordPrompt.module.scss'; + +interface Props { + open: boolean; + wasPasswordIncorrect: boolean; + translations: { + title: string; + headline: string; + description: string; + passwordPlaceholder: string; + wrongPassword: string; + cancel: string; + confirm: string; + }; + onCancel: () => void; + onConfirm: (passphrase: Uint8Array) => void; + isPopup: boolean; +} + +export const EnableAccountPasswordPrompt = ({ + open, + wasPasswordIncorrect, + isPopup, + onConfirm, + onCancel, + translations +}: Props): JSX.Element => { + const [currentPassword, setCurrentPassword] = useState(''); + + return ( + } + onClose={() => { + onCancel(); + setCurrentPassword(''); + }} + popupView={isPopup} + footer={ + + + onConfirm(Buffer.from(currentPassword))} + data-testid="enable-account-password-prompt-confirm-btn" + label={translations.confirm} + /> + + + + } + > + + {translations.headline} + + + {translations.description} + + + + setCurrentPassword(e.target.value)} + errorMessage={wasPasswordIncorrect ? translations.wrongPassword : undefined} + rootStyle={{ width: '100%' }} + /> + + + + ); +}; diff --git a/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/index.ts b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/index.ts new file mode 100644 index 000000000..8a614b23b --- /dev/null +++ b/packages/core/src/ui/components/Account/EnableAccountPasswordPrompt/index.ts @@ -0,0 +1 @@ +export { EnableAccountPasswordPrompt } from './EnableAccountPasswordPrompt'; diff --git a/packages/core/src/ui/components/Account/hooks.ts b/packages/core/src/ui/components/Account/hooks.ts deleted file mode 100644 index 070659f4a..000000000 --- a/packages/core/src/ui/components/Account/hooks.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useState } from 'react'; -import { ProfileDropdown } from '@lace/ui'; - -export const useAccountDataModal = (): { - accountData: ProfileDropdown.AccountData | undefined; - isOpen: boolean; - open: (data: ProfileDropdown.AccountData) => void; - hide: () => void; -} => { - const [dataToEdit, setDataToEdit] = useState(); - - return { - accountData: dataToEdit, - isOpen: dataToEdit !== undefined, - open: (data: ProfileDropdown.AccountData) => { - setDataToEdit(data); - }, - hide: () => { - // eslint-disable-next-line unicorn/no-useless-undefined - setDataToEdit(undefined); - } - }; -}; diff --git a/packages/core/src/ui/components/Account/index.ts b/packages/core/src/ui/components/Account/index.ts index c57eb95ff..4c5b2980c 100644 --- a/packages/core/src/ui/components/Account/index.ts +++ b/packages/core/src/ui/components/Account/index.ts @@ -1,3 +1,4 @@ export * from './EditAccount'; export * from './DisableAccountConfirmation'; -export { useAccountDataModal } from './hooks'; +export * from './EnableAccountPasswordPrompt'; +export * from './EnableAccountConfirmWithHW'; diff --git a/packages/core/src/ui/hooks/index.ts b/packages/core/src/ui/hooks/index.ts index 12f63d698..e0cbd2ca0 100644 --- a/packages/core/src/ui/hooks/index.ts +++ b/packages/core/src/ui/hooks/index.ts @@ -1,3 +1,4 @@ export * from './usePriceFetcher'; export * from './useTranslate'; export * from './useOnClickOutside'; +export * from './useDialogWithData'; diff --git a/packages/core/src/ui/hooks/useDialogWithData.ts b/packages/core/src/ui/hooks/useDialogWithData.ts new file mode 100644 index 000000000..194463dd5 --- /dev/null +++ b/packages/core/src/ui/hooks/useDialogWithData.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +export const useDialogWithData = ( + initialData?: Data +): { + data: Data; + isOpen: boolean; + setData: (data: Data) => void; + open: (data?: Data) => void; + hide: () => void; +} => { + const [isOpen, setIsOpen] = useState(false); + const [data, setData] = useState(initialData); + + return { + data, + isOpen, + setData, + open: (d?: Data) => { + setIsOpen(true); + if (d !== undefined) setData(d); + }, + hide: () => { + setIsOpen(false); + } + }; +}; diff --git a/packages/ui/src/design-system/buttons/call-to-action-button.tsx b/packages/ui/src/design-system/buttons/call-to-action-button.tsx index c94a63a96..7d456b656 100644 --- a/packages/ui/src/design-system/buttons/call-to-action-button.tsx +++ b/packages/ui/src/design-system/buttons/call-to-action-button.tsx @@ -7,7 +7,7 @@ import { SkeletonButton } from './skeleton-button'; import type { ButtonProps } from './skeleton-button'; -type Props = Omit; +type Props = Omit; export const CallToAction = forwardRef( (props, forwardReference): JSX.Element => { diff --git a/packages/ui/src/design-system/password-box/password-box-button.component.tsx b/packages/ui/src/design-system/password-box/password-box-button.component.tsx index d9e9e2f32..9f9822d67 100644 --- a/packages/ui/src/design-system/password-box/password-box-button.component.tsx +++ b/packages/ui/src/design-system/password-box/password-box-button.component.tsx @@ -20,13 +20,13 @@ export const PasswordInputButton = ({ return (