Skip to content
63 changes: 63 additions & 0 deletions ui/helpers/utils/remote-mode.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
10 changes: 10 additions & 0 deletions ui/helpers/utils/remote-mode.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
1 change: 1 addition & 0 deletions ui/pages/pages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ export default function RemoteModeIntroducing() {
color={IconColor.infoDefault}
size={AvatarIconSize.Xl}
/>
<Text variant={TextVariant.headingSm} fontWeight={FontWeight.Bold}>
Introducing Remote Mode
<Text
variant={TextVariant.headingSm}
fontWeight={FontWeight.Bold}
paddingBottom={2}
>
Cold storage. Fast access.
</Text>
<Text variant={TextVariant.bodyMd} color={TextColor.textAlternativeSoft}>
Safely access your hardware wallet funds without plugging it in.
Remote Mode lets you use your hardware wallet without plugging it in.
</Text>
<Box marginTop={4} marginBottom={6}>
<Box
Expand All @@ -42,10 +46,13 @@ export default function RemoteModeIntroducing() {
paddingBottom={2}
>
<Icon name={IconName.SwapHorizontal} color={IconColor.infoDefault} />
<Text>
Easier yet safe to trade with cold funds. Never miss a market
opportunity.
</Text>
<Text
fontWeight={FontWeight.Bold}
style={{ display: 'inline-block' }}
>
Stay secure.
</Text>{' '}
Your keys stay offline, and your funds stay in cold storage.
</Box>
<Box
display={Display.Flex}
Expand All @@ -55,10 +62,13 @@ export default function RemoteModeIntroducing() {
paddingBottom={2}
>
<Icon name={IconName.WalletCard} color={IconColor.infoDefault} />
<Text>
Use allowances for transactions, limiting exposure of cold funds &
keys.
</Text>
<Text
fontWeight={FontWeight.Bold}
style={{ display: 'inline-block' }}
>
Move faster.
</Text>{' '}
Allow limited actions like swaps or approvals ahead of time.
</Box>
<Box
display={Display.Flex}
Expand All @@ -68,10 +78,13 @@ export default function RemoteModeIntroducing() {
paddingBottom={2}
>
<Icon name={IconName.SecurityTick} color={IconColor.infoDefault} />
<Text>
Set your terms with spending caps & other smart contract enforced
rules.
</Text>
<Text
fontWeight={FontWeight.Bold}
style={{ display: 'inline-block' }}
>
Stay in control.
</Text>{' '}
Set your own rules, like spending caps and allowed actions.
</Box>
<Box
display={Display.Flex}
Expand All @@ -81,9 +94,13 @@ export default function RemoteModeIntroducing() {
paddingBottom={2}
>
<Icon name={IconName.Star} color={IconColor.infoDefault} />
<Text>
Get all the benefits of a smart account, and switch back anytime.
</Text>
<Text
fontWeight={FontWeight.Bold}
style={{ display: 'inline-block' }}
>
Get smart.
</Text>{' '}
All the benefits of a smart account, and your keys stay safe.
</Box>
</Box>
</Box>
Expand Down
81 changes: 71 additions & 10 deletions ui/pages/remote-mode/overview/remote-mode-overview.container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,9 +48,16 @@ export default function RemoteModeIntroducing() {
const [currentScreen, setCurrentScreen] = useState<RemoteScreen>(
RemoteScreen.OVERVIEW,
);
const [isHardwareAccount, setIsHardwareAccount] = useState<boolean>(false);

const selectedHardwareAccount = useSelector(getSelectedInternalAccount);
const history = useHistory();
const isRemoteModeEnabled = useSelector(getIsRemoteModeEnabled);

useEffect(() => {
setIsHardwareAccount(isRemoteModeSupported(selectedHardwareAccount));
}, [selectedHardwareAccount]);

useEffect(() => {
if (!isRemoteModeEnabled) {
history.push(DEFAULT_ROUTE);
Expand All @@ -39,21 +68,22 @@ export default function RemoteModeIntroducing() {
switch (currentScreen) {
case RemoteScreen.OVERVIEW:
return (
<Box padding={6}>
<Content padding={6}>
<RemoteModeOverview />
<Button
style={{ width: '100%' }}
onClick={() => setCurrentScreen(RemoteScreen.PERMISSIONS)}
size={ButtonSize.Lg}
disabled={!isHardwareAccount}
>
Continue
Get Remote Mode
</Button>
</Box>
</Content>
);

case RemoteScreen.PERMISSIONS:
return (
<Box padding={6}>
<Content padding={6}>
<RemoteModePermissions
setStartEnableRemoteSwap={() => {
history.push(REMOTE_ROUTE_SETUP_SWAPS);
Expand All @@ -62,24 +92,55 @@ export default function RemoteModeIntroducing() {
history.push(REMOTE_ROUTE_SETUP_DAILY_ALLOWANCE);
}}
/>
</Box>
</Content>
);

case RemoteScreen.SETUP_REMOTE_SWAPS:
return (
<Box padding={2}>
<Content padding={2}>
<RemoteModeSetup />
</Box>
</Content>
);

default:
return null;
}
};

const onCancel = () => {
history.push(DEFAULT_ROUTE);
};

return (
<div className="main-container" data-testid="remote-mode">
<Page className="main-container" data-testid="remote-mode">
<Header
textProps={{
variant: TextVariant.headingSm,
}}
startAccessory={
<ButtonIcon
size={ButtonIconSize.Sm}
ariaLabel={'back'}
iconName={IconName.ArrowLeft}
onClick={onCancel}
/>
}
>
Remote mode
</Header>
{!isHardwareAccount && (
<Box padding={4}>
<BannerAlert severity={BannerAlertSeverity.Warning} marginBottom={2}>
<Text variant={TextVariant.headingSm} fontWeight={FontWeight.Bold}>
Select a hardware wallet
</Text>
<Text variant={TextVariant.bodyMd}>
To continue, select your hardware wallet from the account menu.
</Text>
</BannerAlert>
</Box>
)}
{renderScreen()}
</div>
</Page>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -32,9 +31,6 @@ export default function RemoteModePermissions({

return (
<Box>
<Text variant={TextVariant.headingSm} fontWeight={FontWeight.Bold}>
Permissions
</Text>
<Text variant={TextVariant.bodyMd} color={TextColor.textAlternativeSoft}>
Safely access your hardware wallet funds without plugging it in. Revoke
permissions anytime.
Expand All @@ -48,13 +44,13 @@ export default function RemoteModePermissions({
paddingTop={2}
paddingBottom={2}
>
<Text>Swap</Text>
<Text>Remote Swaps</Text>
<Text
color={TextColor.infoDefault}
style={{ cursor: 'pointer' }}
onClick={handleEnableRemoteSwap}
>
Enable
Turn on
</Text>
</Box>
<Text color={TextColor.textAlternativeSoft}>
Expand All @@ -72,12 +68,13 @@ export default function RemoteModePermissions({
paddingTop={2}
paddingBottom={2}
>
<Text>Daily allowances</Text>
<Text>Withdrawal limit</Text>
<Text
color={TextColor.infoDefault}
style={{ cursor: 'pointer' }}
onClick={handleEnableDailyAllowance}
>
Enable
Turn on
</Text>
</Box>
<Text color={TextColor.textAlternativeSoft}>
Expand Down
Loading
Loading