diff --git a/ui/helpers/utils/remote-mode.test.ts b/ui/helpers/utils/remote-mode.test.ts new file mode 100644 index 000000000000..8c14d140e716 --- /dev/null +++ b/ui/helpers/utils/remote-mode.test.ts @@ -0,0 +1,63 @@ +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { isRemoteModeSupported } from './remote-mode'; + +describe('Remote Mode Utils', () => { + describe('isRemoteModeSupported', () => { + it('returns true for supported hardware wallet types', () => { + const ledgerAccount: InternalAccount = { + address: '0x12C7e...q135f', + type: 'eip155:eoa', + id: '1', + options: {}, + metadata: { + name: 'Ledger Hardware', + importTime: 1717334400, + keyring: { + type: 'Ledger Hardware', + }, + }, + scopes: [], + methods: [], + }; + + const latticeAccount: InternalAccount = { + address: '0x12C7e...q135f', + type: 'eip155:eoa', + id: '2', + options: {}, + metadata: { + name: 'Lattice Hardware', + importTime: 1717334400, + keyring: { + type: 'Lattice Hardware', + }, + }, + scopes: [], + methods: [], + }; + + expect(isRemoteModeSupported(ledgerAccount)).toBe(true); + expect(isRemoteModeSupported(latticeAccount)).toBe(true); + }); + + it('returns false for unsupported hardware wallet types', () => { + const unsupportedAccount: InternalAccount = { + address: '0x12C7e...q135f', + type: 'eip155:eoa', + id: '3', + options: {}, + metadata: { + name: 'Some Other Wallet', + importTime: 1717334400, + keyring: { + type: 'eip155', + }, + }, + scopes: [], + methods: [], + }; + + expect(isRemoteModeSupported(unsupportedAccount)).toBe(false); + }); + }); +}); diff --git a/ui/helpers/utils/remote-mode.ts b/ui/helpers/utils/remote-mode.ts new file mode 100644 index 000000000000..e7da3e18b725 --- /dev/null +++ b/ui/helpers/utils/remote-mode.ts @@ -0,0 +1,10 @@ +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +const SUPPORTED_HARDWARE_WALLET_TYPES = ['Ledger Hardware', 'Lattice Hardware']; + +export function isRemoteModeSupported(account: InternalAccount) { + // todo: add check that account also implements signEip7702Authorization() + return SUPPORTED_HARDWARE_WALLET_TYPES.includes( + account.metadata.keyring.type, + ); +} diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 887850271bf0..4a9f79f343e7 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -21,6 +21,7 @@ @import 'snaps/snaps-list/index'; @import 'snaps/snap-view/index'; @import 'create-snap-account/index'; +@import 'remote-mode/setup/setup-swaps/index'; @import 'remove-snap-account/index'; @import 'swaps/index'; @import 'bridge/index'; diff --git a/ui/pages/remote-mode/introducing/remote-mode-introducing.component.tsx b/ui/pages/remote-mode/introducing/remote-mode-introducing.component.tsx index 3b3e834e9a93..5f93ecc6a2c8 100644 --- a/ui/pages/remote-mode/introducing/remote-mode-introducing.component.tsx +++ b/ui/pages/remote-mode/introducing/remote-mode-introducing.component.tsx @@ -27,11 +27,15 @@ export default function RemoteModeIntroducing() { color={IconColor.infoDefault} size={AvatarIconSize.Xl} /> - - Introducing Remote Mode + + Cold storage. Fast access. - Safely access your hardware wallet funds without plugging it in. + Remote Mode lets you use your hardware wallet without plugging it in. - - Easier yet safe to trade with cold funds. Never miss a market - opportunity. - + + Stay secure. + {' '} + Your keys stay offline, and your funds stay in cold storage. - - Use allowances for transactions, limiting exposure of cold funds & - keys. - + + Move faster. + {' '} + Allow limited actions like swaps or approvals ahead of time. - - Set your terms with spending caps & other smart contract enforced - rules. - + + Stay in control. + {' '} + Set your own rules, like spending caps and allowed actions. - - Get all the benefits of a smart account, and switch back anytime. - + + Get smart. + {' '} + All the benefits of a smart account, and your keys stay safe. diff --git a/ui/pages/remote-mode/overview/remote-mode-overview.container.tsx b/ui/pages/remote-mode/overview/remote-mode-overview.container.tsx index c211ca32b982..3d19ba713bd7 100644 --- a/ui/pages/remote-mode/overview/remote-mode-overview.container.tsx +++ b/ui/pages/remote-mode/overview/remote-mode-overview.container.tsx @@ -2,8 +2,30 @@ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getSelectedInternalAccount } from '../../../selectors'; import { getIsRemoteModeEnabled } from '../../../selectors/remote-mode'; -import { Button, Box, ButtonSize } from '../../../components/component-library'; +import { isRemoteModeSupported } from '../../../helpers/utils/remote-mode'; + +import { + BannerAlert, + BannerAlertSeverity, + Box, + Button, + ButtonIcon, + ButtonSize, + ButtonIconSize, + IconName, + Text, +} from '../../../components/component-library'; +import { + TextVariant, + FontWeight, +} from '../../../helpers/constants/design-system'; +import { + Content, + Header, + Page, +} from '../../../components/multichain/pages/page'; import { DEFAULT_ROUTE, @@ -26,9 +48,16 @@ export default function RemoteModeIntroducing() { const [currentScreen, setCurrentScreen] = useState( RemoteScreen.OVERVIEW, ); + const [isHardwareAccount, setIsHardwareAccount] = useState(false); + + const selectedHardwareAccount = useSelector(getSelectedInternalAccount); const history = useHistory(); const isRemoteModeEnabled = useSelector(getIsRemoteModeEnabled); + useEffect(() => { + setIsHardwareAccount(isRemoteModeSupported(selectedHardwareAccount)); + }, [selectedHardwareAccount]); + useEffect(() => { if (!isRemoteModeEnabled) { history.push(DEFAULT_ROUTE); @@ -39,21 +68,22 @@ export default function RemoteModeIntroducing() { switch (currentScreen) { case RemoteScreen.OVERVIEW: return ( - + - + ); case RemoteScreen.PERMISSIONS: return ( - + { history.push(REMOTE_ROUTE_SETUP_SWAPS); @@ -62,14 +92,14 @@ export default function RemoteModeIntroducing() { history.push(REMOTE_ROUTE_SETUP_DAILY_ALLOWANCE); }} /> - + ); case RemoteScreen.SETUP_REMOTE_SWAPS: return ( - + - + ); default: @@ -77,9 +107,40 @@ export default function RemoteModeIntroducing() { } }; + const onCancel = () => { + history.push(DEFAULT_ROUTE); + }; + return ( -
+ +
+ } + > + Remote mode +
+ {!isHardwareAccount && ( + + + + Select a hardware wallet + + + To continue, select your hardware wallet from the account menu. + + + + )} {renderScreen()} -
+ ); } diff --git a/ui/pages/remote-mode/overview/remote-mode-permissions.component.tsx b/ui/pages/remote-mode/overview/remote-mode-permissions.component.tsx index 1cf469f2fb6c..214c844a6608 100644 --- a/ui/pages/remote-mode/overview/remote-mode-permissions.component.tsx +++ b/ui/pages/remote-mode/overview/remote-mode-permissions.component.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Box, Text } from '../../../components/component-library'; import Card from '../../../components/ui/card'; import { - FontWeight, TextVariant, Display, JustifyContent, @@ -32,9 +31,6 @@ export default function RemoteModePermissions({ return ( - - Permissions - Safely access your hardware wallet funds without plugging it in. Revoke permissions anytime. @@ -48,13 +44,13 @@ export default function RemoteModePermissions({ paddingTop={2} paddingBottom={2} > - Swap + Remote Swaps - Enable + Turn on @@ -72,12 +68,13 @@ export default function RemoteModePermissions({ paddingTop={2} paddingBottom={2} > - Daily allowances + Withdrawal limit - Enable + Turn on diff --git a/ui/pages/remote-mode/setup/setup-daily-allowance/remote-mode-setup-daily-allowance.component.tsx b/ui/pages/remote-mode/setup/setup-daily-allowance/remote-mode-setup-daily-allowance.component.tsx index 5ba9dd9ed921..34426da0b1d9 100644 --- a/ui/pages/remote-mode/setup/setup-daily-allowance/remote-mode-setup-daily-allowance.component.tsx +++ b/ui/pages/remote-mode/setup/setup-daily-allowance/remote-mode-setup-daily-allowance.component.tsx @@ -4,8 +4,18 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { + Content, + Footer, + Header, + Page, +} from '../../../../components/multichain/pages/page'; +import { + BannerAlert, + BannerAlertSeverity, Box, Button, + ButtonIcon, + ButtonIconSize, ButtonVariant, ButtonSize, Text, @@ -48,6 +58,7 @@ import { import RemoteModeHardwareWalletConfirm from '../hardware-wallet-confirm-modal'; import RemoteModeDailyAllowanceCard from '../daily-allowance-card'; import StepIndicator from '../step-indicator/step-indicator.component'; +import { isRemoteModeSupported } from '../../../../helpers/utils/remote-mode'; const TOTAL_STEPS = 3; @@ -72,6 +83,7 @@ export default function RemoteModeSetupDailyAllowance() { useState(false); const [selectedAccount, setSelectedAccount] = useState(null); + const [isHardwareAccount, setIsHardwareAccount] = useState(false); const selectedHardwareAccount = useSelector(getSelectedInternalAccount); const authorizedAccounts: InternalAccountWithBalance[] = useSelector( @@ -82,6 +94,10 @@ export default function RemoteModeSetupDailyAllowance() { const isRemoteModeEnabled = useSelector(getIsRemoteModeEnabled); + useEffect(() => { + setIsHardwareAccount(isRemoteModeSupported(selectedHardwareAccount)); + }, [selectedHardwareAccount]); + useEffect(() => { if (authorizedAccounts.length > 0) { setSelectedAccount(authorizedAccounts[0]); @@ -146,6 +162,10 @@ export default function RemoteModeSetupDailyAllowance() { history.replace(REMOTE_ROUTE); }; + const onCancel = () => { + history.goBack(); + }; + const renderStepContent = () => { switch (currentStep) { case 1: @@ -471,16 +491,41 @@ export default function RemoteModeSetupDailyAllowance() { }; return ( -
- +
+ } + > + Remote mode +
+ + {!isHardwareAccount && ( + + + Select a hardware wallet + + + To continue, select your hardware wallet from the account menu. + + + )} - - - -
- -
+ + +
+ + +
+ ); } diff --git a/ui/pages/remote-mode/setup/setup-swaps/index.scss b/ui/pages/remote-mode/setup/setup-swaps/index.scss new file mode 100644 index 000000000000..8799fb95458d --- /dev/null +++ b/ui/pages/remote-mode/setup/setup-swaps/index.scss @@ -0,0 +1,13 @@ +@use "design-system"; + +.unit-input { + width: 100%; + border-radius: 0.5rem; + min-height: 45px; + margin-top: 8px; +} + +// override the default width of the unit input as the placeholder text is being truncated +.unit-input .unit-input__input { + width: 100% !important; +} diff --git a/ui/pages/remote-mode/setup/setup-swaps/remote-mode-setup-swaps.component.tsx b/ui/pages/remote-mode/setup/setup-swaps/remote-mode-setup-swaps.component.tsx index 7a11431cc42b..f598b0a56dac 100644 --- a/ui/pages/remote-mode/setup/setup-swaps/remote-mode-setup-swaps.component.tsx +++ b/ui/pages/remote-mode/setup/setup-swaps/remote-mode-setup-swaps.component.tsx @@ -4,15 +4,21 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { + AvatarAccount, + AvatarAccountSize, + AvatarAccountVariant, + BannerAlert, + BannerAlertSeverity, Box, Button, + ButtonIcon, + ButtonIconSize, ButtonVariant, ButtonSize, Text, Icon, IconName, IconSize, - Tag, } from '../../../../components/component-library'; import Tooltip from '../../../../components/ui/tooltip'; import UnitInput from '../../../../components/ui/unit-input'; @@ -34,6 +40,13 @@ import { import Card from '../../../../components/ui/card'; import { AccountPicker } from '../../../../components/multichain/account-picker'; import { AccountListMenu } from '../../../../components/multichain/account-list-menu'; +import { + Content, + Footer, + Header, + Page, +} from '../../../../components/multichain/pages/page'; + import { SwapAllowance, TokenSymbol, ToTokenOption } from '../../remote.types'; import { DEFAULT_ROUTE, @@ -43,6 +56,7 @@ import { getIsRemoteModeEnabled } from '../../../../selectors/remote-mode'; import RemoteModeHardwareWalletConfirm from '../hardware-wallet-confirm-modal'; import RemoteModeSwapAllowanceCard from '../swap-allowance-card'; import StepIndicator from '../step-indicator/step-indicator.component'; +import { isRemoteModeSupported } from '../../../../helpers/utils/remote-mode'; import { InternalAccountWithBalance } from '../../../../selectors/selectors.types'; import { @@ -77,6 +91,7 @@ export default function RemoteModeSetupSwaps() { useState(false); const [selectedAccount, setSelectedAccount] = useState(null); + const [isHardwareAccount, setIsHardwareAccount] = useState(false); const selectedHardwareAccount = useSelector(getSelectedInternalAccount); const authorizedAccounts: InternalAccountWithBalance[] = useSelector( @@ -87,6 +102,10 @@ export default function RemoteModeSetupSwaps() { const isRemoteModeEnabled = useSelector(getIsRemoteModeEnabled); + useEffect(() => { + setIsHardwareAccount(isRemoteModeSupported(selectedHardwareAccount)); + }, [selectedHardwareAccount]); + useEffect(() => { if (authorizedAccounts.length > 0) { setSelectedAccount(authorizedAccounts[0]); @@ -150,6 +169,10 @@ export default function RemoteModeSetupSwaps() { history.replace(REMOTE_ROUTE); }; + const onCancel = () => { + history.goBack(); + }; + const renderStepContent = () => { switch (currentStep) { case 1: @@ -217,7 +240,15 @@ export default function RemoteModeSetupSwaps() { - {selectedHardwareAccount.metadata.name} + + + {selectedHardwareAccount.metadata.name} + @@ -226,7 +257,7 @@ export default function RemoteModeSetupSwaps() { marginBottom={2} > - Allowances + Swap limit @@ -300,9 +336,13 @@ export default function RemoteModeSetupSwaps() { style={{ width: '100%' }} /> - - Allow trading for any token. Higher risk option, in case the - authorized account gets compromised. + + Tip: This is a higher risk option if your authorized account + is compromised. - - - - + + + ); }