From fcac90df6dc7bcb2ac5540a7580a130de75dd1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Mon, 29 Apr 2024 22:49:30 +0200 Subject: [PATCH] feat(suite-native): new device switcher --- suite-common/icons/assets/icons/linkChain.svg | 4 - .../icons/assets/icons/linkChainBroken.svg | 3 - suite-common/icons/src/icons.ts | 2 - suite-common/suite-utils/src/device.ts | 14 ++ .../wallet-core/src/device/deviceReducer.ts | 21 +- suite-native/atoms/src/TextDivider.tsx | 9 +- suite-native/device-manager/package.json | 1 + .../src/components/AddHiddenWalletButton.tsx | 23 ++- .../src/components/DeviceAction.tsx | 60 ++++++ .../src/components/DeviceControlButtons.tsx | 62 ------ .../src/components/DeviceInfoButton.tsx | 68 +++++++ .../src/components/DeviceItem.tsx | 68 ------- .../components/DeviceItem/ConnectionDot.tsx | 26 +++ .../src/components/DeviceItem/DeviceItem.tsx | 33 ++++ .../DeviceItem/DeviceItemContent.tsx | 103 ++++++++++ .../components/DeviceItem/DeviceItemIcon.tsx | 31 +++ .../DeviceItem/SimpleDeviceItemContent.tsx | 95 +++++++++ .../WalletDetailDeviceItemContent.tsx | 77 ++++++++ .../src/components/DeviceItemContent.tsx | 84 -------- .../src/components/DeviceList.tsx | 181 ++++++++++++++++++ .../src/components/DeviceManagerContent.tsx | 132 +++++++------ .../src/components/DeviceManagerModal.tsx | 69 +++++-- .../src/components/DeviceModelIcon.tsx | 18 ++ .../src/components/DeviceSwitch.tsx | 41 ++-- .../src/components/DevicesToggleButton.tsx | 26 +++ .../PortfolioTrackerDeviceManagerContent.tsx | 53 +++-- .../src/components/WalletItem.tsx | 76 ++++++++ .../src/components/WalletList.tsx | 34 ++++ suite-native/device-manager/src/index.ts | 1 + suite-native/device-manager/tsconfig.json | 3 + suite-native/device/package.json | 2 + suite-native/device/tsconfig.json | 6 + suite-native/intl/src/en.ts | 21 +- .../src/components/TrezorModelIcon.tsx | 20 -- .../components/ViewOnly/DevicesManagement.tsx | 62 +++--- .../components/ViewOnly/TrezorModelIcon.tsx | 20 -- .../src/components/ViewOnly/WalletRow.tsx | 4 +- yarn.lock | 3 + 38 files changed, 1116 insertions(+), 440 deletions(-) delete mode 100644 suite-common/icons/assets/icons/linkChain.svg delete mode 100644 suite-common/icons/assets/icons/linkChainBroken.svg create mode 100644 suite-native/device-manager/src/components/DeviceAction.tsx delete mode 100644 suite-native/device-manager/src/components/DeviceControlButtons.tsx create mode 100644 suite-native/device-manager/src/components/DeviceInfoButton.tsx delete mode 100644 suite-native/device-manager/src/components/DeviceItem.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/ConnectionDot.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/DeviceItem.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/DeviceItemContent.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/DeviceItemIcon.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/SimpleDeviceItemContent.tsx create mode 100644 suite-native/device-manager/src/components/DeviceItem/WalletDetailDeviceItemContent.tsx delete mode 100644 suite-native/device-manager/src/components/DeviceItemContent.tsx create mode 100644 suite-native/device-manager/src/components/DeviceList.tsx create mode 100644 suite-native/device-manager/src/components/DeviceModelIcon.tsx create mode 100644 suite-native/device-manager/src/components/DevicesToggleButton.tsx create mode 100644 suite-native/device-manager/src/components/WalletItem.tsx create mode 100644 suite-native/device-manager/src/components/WalletList.tsx delete mode 100644 suite-native/module-settings/src/components/TrezorModelIcon.tsx delete mode 100644 suite-native/module-settings/src/components/ViewOnly/TrezorModelIcon.tsx diff --git a/suite-common/icons/assets/icons/linkChain.svg b/suite-common/icons/assets/icons/linkChain.svg deleted file mode 100644 index baf197562b7..00000000000 --- a/suite-common/icons/assets/icons/linkChain.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/suite-common/icons/assets/icons/linkChainBroken.svg b/suite-common/icons/assets/icons/linkChainBroken.svg deleted file mode 100644 index d04f0182f98..00000000000 --- a/suite-common/icons/assets/icons/linkChainBroken.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/suite-common/icons/src/icons.ts b/suite-common/icons/src/icons.ts index 7336ffb2c24..a61e5cbde4f 100644 --- a/suite-common/icons/src/icons.ts +++ b/suite-common/icons/src/icons.ts @@ -57,8 +57,6 @@ export const icons = { label: require('../assets/icons/label.svg'), lifebuoy: require('../assets/icons/lifebuoy.svg'), link: require('../assets/icons/link.svg'), - linkChain: require('../assets/icons/linkChain.svg'), - linkChainBroken: require('../assets/icons/linkChainBroken.svg'), lock: require('../assets/icons/lock.svg'), minus: require('../assets/icons/minus.svg'), minusCircle: require('../assets/icons/minusCircle.svg'), diff --git a/suite-common/suite-utils/src/device.ts b/suite-common/suite-utils/src/device.ts index 8f94d483460..d9eaab80fc0 100644 --- a/suite-common/suite-utils/src/device.ts +++ b/suite-common/suite-utils/src/device.ts @@ -409,6 +409,20 @@ export const getPhysicalDeviceUniqueIds = (devices: Device[]) => export const getPhysicalDeviceCount = (devices: Device[]) => getPhysicalDeviceUniqueIds(devices).length; +export const getSortedDevicesWithoutInstances = ( + devices: TrezorDevice[], + excludedDeviceId?: string | null, +) => + getDeviceInstancesGroupedByDeviceId(devices) + .flatMap(group => group[0]) + .filter(d => d?.id !== excludedDeviceId && d?.id) + .sort((a, b) => { + if (!a.connected) return -1; + if (!b.connected) return 1; + + return 0; + }); + export const parseFirmwareChangelog = (release?: FirmwareRelease) => { if (!release?.changelog?.length || !release) { return null; diff --git a/suite-common/wallet-core/src/device/deviceReducer.ts b/suite-common/wallet-core/src/device/deviceReducer.ts index a9e04b3aeed..b27c0822c2b 100644 --- a/suite-common/wallet-core/src/device/deviceReducer.ts +++ b/suite-common/wallet-core/src/device/deviceReducer.ts @@ -1,7 +1,7 @@ import { memoize } from 'proxy-memoize'; import * as deviceUtils from '@suite-common/suite-utils'; -import { getStatus } from '@suite-common/suite-utils'; +import { getDeviceInstances, getStatus } from '@suite-common/suite-utils'; import { Device, Features, UI } from '@trezor/connect'; import { getFirmwareVersion, getFirmwareVersionArray } from '@trezor/device-utils'; import { Network, networks } from '@suite-common/wallet-config'; @@ -858,3 +858,22 @@ export const selectDeviceState = (state: DeviceRootState) => { return device?.state ?? null; }; + +export const selectDeviceInstances = memoize((state: DeviceRootState) => { + const device = selectDevice(state); + + if (!device) { + return []; + } + + const allDevices = selectDevices(state); + + return getDeviceInstances(device, allDevices); +}); + +export const selectInstacelessUnselectedDevices = memoize((state: DeviceRootState) => { + const device = selectDevice(state); + const allDevices = selectDevices(state); + + return deviceUtils.getSortedDevicesWithoutInstances(allDevices, device?.id); +}); diff --git a/suite-native/atoms/src/TextDivider.tsx b/suite-native/atoms/src/TextDivider.tsx index fc66e300bd6..67f10ee00f9 100644 --- a/suite-native/atoms/src/TextDivider.tsx +++ b/suite-native/atoms/src/TextDivider.tsx @@ -12,16 +12,17 @@ type TextDividerProps = { const separatorStyle = prepareNativeStyle<{ horizontalMargin?: number }>( (utils, { horizontalMargin }) => ({ - borderColor: utils.colors.borderElevation0, - borderWidth: utils.borders.widths.small, + backgroundColor: utils.colors.borderElevation0, + height: utils.borders.widths.small, flex: 1, // We want the separator to be full width, but we need to offset it by the parent padding marginHorizontal: typeof horizontalMargin === 'number' ? -horizontalMargin : 0, }), ); -const separatorTitleStyle = prepareNativeStyle(_ => ({ +const separatorTitleStyle = prepareNativeStyle(utils => ({ paddingHorizontal: 12, + paddingVertical: utils.spacings.extraSmall, })); export const TextDivider = ({ title, horizontalMargin = 0 }: TextDividerProps) => { @@ -31,7 +32,7 @@ export const TextDivider = ({ title, horizontalMargin = 0 }: TextDividerProps) = - + diff --git a/suite-native/device-manager/package.json b/suite-native/device-manager/package.json index 44bacbeabc3..10db41e4701 100644 --- a/suite-native/device-manager/package.json +++ b/suite-native/device-manager/package.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "1.9.5", "@suite-common/icons": "workspace:*", "@suite-common/suite-types": "workspace:*", + "@suite-common/suite-utils": "workspace:*", "@suite-common/wallet-core": "workspace:*", "@suite-native/analytics": "workspace:*", "@suite-native/atoms": "workspace:*", diff --git a/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx b/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx index 920818253c8..5b1ff8d54eb 100644 --- a/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx +++ b/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx @@ -1,12 +1,20 @@ import { useDispatch, useSelector } from 'react-redux'; -import { Button } from '@suite-native/atoms'; import { Translation } from '@suite-native/intl'; import { createDeviceInstance, selectDevice } from '@suite-common/wallet-core'; +import { Text } from '@suite-native/atoms'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { Icon } from '@suite-common/icons'; import { useDeviceManager } from '../hooks/useDeviceManager'; +import { DeviceAction } from './DeviceAction'; + +const textStyle = prepareNativeStyle(_ => ({ + flex: 1, +})); export const AddHiddenWalletButton = () => { + const { applyStyle } = useNativeStyles(); const dispatch = useDispatch(); const device = useSelector(selectDevice); @@ -21,8 +29,15 @@ export const AddHiddenWalletButton = () => { }; return ( - + + + + + + ); }; diff --git a/suite-native/device-manager/src/components/DeviceAction.tsx b/suite-native/device-manager/src/components/DeviceAction.tsx new file mode 100644 index 00000000000..e97c45d8e4d --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceAction.tsx @@ -0,0 +1,60 @@ +import { ReactNode } from 'react'; +import { Pressable } from 'react-native'; + +import { HStack } from '@suite-native/atoms'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +type DeviceActionProps = { + testID: string; + onPress: () => void; + children: ReactNode; + flex?: number; + showAsFullWidth?: boolean; +}; + +const contentStyle = prepareNativeStyle(utils => ({ + paddingHorizontal: utils.spacings.medium, + paddingVertical: 12, + alignItems: 'center', + height: 44, + gap: utils.spacings.small, + backgroundColor: utils.colors.backgroundSurfaceElevation1, + borderWidth: utils.borders.widths.small, + borderRadius: 12, + borderColor: utils.colors.borderElevation1, +})); + +const pressableStyle = prepareNativeStyle<{ showAsFullWidth: boolean; flex: number | undefined }>( + (_, { showAsFullWidth, flex }) => { + return { + flex, + extend: { + condition: showAsFullWidth, + style: { + flex: 1, + justifyContent: 'center', + }, + }, + }; + }, +); + +export const DeviceAction = ({ + testID, + onPress, + children, + flex, + showAsFullWidth = false, +}: DeviceActionProps) => { + const { applyStyle } = useNativeStyles(); + + return ( + + {children} + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceControlButtons.tsx b/suite-native/device-manager/src/components/DeviceControlButtons.tsx deleted file mode 100644 index df8bf131939..00000000000 --- a/suite-native/device-manager/src/components/DeviceControlButtons.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useDispatch, useSelector } from 'react-redux'; - -import { useNavigation } from '@react-navigation/native'; - -import { analytics, EventType } from '@suite-native/analytics'; -import { Box, Button, HStack } from '@suite-native/atoms'; -import { deviceActions, selectDevice } from '@suite-common/wallet-core'; -import { Translation } from '@suite-native/intl'; -import { - RootStackParamList, - RootStackRoutes, - StackToStackCompositeNavigationProps, -} from '@suite-native/navigation'; - -import { useDeviceManager } from '../hooks/useDeviceManager'; - -type NavigationProp = StackToStackCompositeNavigationProps< - RootStackParamList, - RootStackRoutes.AppTabs, - RootStackParamList ->; - -export const DeviceControlButtons = () => { - const selectedDevice = useSelector(selectDevice); - - const { setIsDeviceManagerVisible } = useDeviceManager(); - - const navigation = useNavigation(); - const dispatch = useDispatch(); - - if (!selectedDevice) return null; - - const handleEject = () => { - setIsDeviceManagerVisible(false); - dispatch(deviceActions.deviceDisconnect(selectedDevice)); - analytics.report({ - type: EventType.EjectDeviceClick, - payload: { origin: 'deviceManager' }, - }); - }; - - const handleDeviceRedirect = () => { - setIsDeviceManagerVisible(false); - navigation.navigate(RootStackRoutes.DeviceInfo); - analytics.report({ type: EventType.DeviceManagerClick, payload: { action: 'deviceInfo' } }); - }; - - return ( - - - - - - - - - ); -}; diff --git a/suite-native/device-manager/src/components/DeviceInfoButton.tsx b/suite-native/device-manager/src/components/DeviceInfoButton.tsx new file mode 100644 index 00000000000..b449994c3e3 --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceInfoButton.tsx @@ -0,0 +1,68 @@ +import { useSelector } from 'react-redux'; + +import { useNavigation } from '@react-navigation/native'; + +import { analytics, EventType } from '@suite-native/analytics'; +import { HStack, Text } from '@suite-native/atoms'; +import { selectDevice } from '@suite-common/wallet-core'; +import { Translation } from '@suite-native/intl'; +import { + RootStackParamList, + RootStackRoutes, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; +import { Icon } from '@suite-common/icons'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { useDeviceManager } from '../hooks/useDeviceManager'; +import { DeviceAction } from './DeviceAction'; + +type NavigationProp = StackToStackCompositeNavigationProps< + RootStackParamList, + RootStackRoutes.AppTabs, + RootStackParamList +>; + +type DeviceInfoButtonProps = { + showAsFullWidth: boolean; +}; + +const contentStyle = prepareNativeStyle<{ showAsFullWidth: boolean }>((_, { showAsFullWidth }) => ({ + extend: { + condition: showAsFullWidth, + style: { + flex: 1, + justifyContent: 'center', + }, + }, +})); + +export const DeviceInfoButton = ({ showAsFullWidth }: DeviceInfoButtonProps) => { + const { applyStyle } = useNativeStyles(); + const navigation = useNavigation(); + const { setIsDeviceManagerVisible } = useDeviceManager(); + const selectedDevice = useSelector(selectDevice); + + const handleDeviceRedirect = () => { + setIsDeviceManagerVisible(false); + navigation.navigate(RootStackRoutes.DeviceInfo); + analytics.report({ type: EventType.DeviceManagerClick, payload: { action: 'deviceInfo' } }); + }; + + if (!selectedDevice) return null; + + return ( + + + + + + + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceItem.tsx b/suite-native/device-manager/src/components/DeviceItem.tsx deleted file mode 100644 index 1225293b18e..00000000000 --- a/suite-native/device-manager/src/components/DeviceItem.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useDispatch, useSelector } from 'react-redux'; -import { Pressable } from 'react-native'; - -import { analytics, EventType } from '@suite-native/analytics'; -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { HStack } from '@suite-native/atoms'; -import { - DeviceRootState, - PORTFOLIO_TRACKER_DEVICE_ID, - selectDeviceById, - selectDeviceId, - selectDeviceThunk, -} from '@suite-common/wallet-core'; -import { Icon } from '@suite-common/icons'; -import { TrezorDevice } from '@suite-common/suite-types'; - -import { useDeviceManager } from '../hooks/useDeviceManager'; -import { DeviceItemContent } from './DeviceItemContent'; - -type DeviceItemProps = { - id: TrezorDevice['id']; -}; - -const deviceItemWrapperStyle = prepareNativeStyle(utils => ({ - justifyContent: 'space-between', - alignItems: 'center', - borderRadius: utils.borders.radii.medium, - backgroundColor: utils.colors.backgroundTertiaryDefaultOnElevation1, - paddingHorizontal: utils.spacings.medium, - paddingVertical: 12, -})); - -export const DeviceItem = ({ id: deviceItemId }: DeviceItemProps) => { - const dispatch = useDispatch(); - const { applyStyle } = useNativeStyles(); - - const selectedDeviceId = useSelector(selectDeviceId); - const device = useSelector((state: DeviceRootState) => selectDeviceById(state, deviceItemId)); - - const { setIsDeviceManagerVisible } = useDeviceManager(); - - const handleSelectDevice = () => { - setIsDeviceManagerVisible(false); - - if (deviceItemId === selectedDeviceId) return; - - dispatch(selectDeviceThunk(device)); - - analytics.report({ - type: EventType.DeviceManagerClick, - payload: { - action: - deviceItemId === PORTFOLIO_TRACKER_DEVICE_ID - ? 'portfolioTracker' - : 'connectDeviceButton', - }, - }); - }; - - return ( - - - - - - - ); -}; diff --git a/suite-native/device-manager/src/components/DeviceItem/ConnectionDot.tsx b/suite-native/device-manager/src/components/DeviceItem/ConnectionDot.tsx new file mode 100644 index 00000000000..488fdc1376f --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/ConnectionDot.tsx @@ -0,0 +1,26 @@ +import { View } from 'react-native'; + +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +type ConnectionDotProps = { + isConnected: boolean; +}; + +const dotStyle = prepareNativeStyle<{ isConnected: boolean }>((utils, { isConnected }) => ({ + width: utils.spacings.small, + height: utils.spacings.small, + borderRadius: utils.borders.radii.round, + backgroundColor: utils.colors.textSubdued, + extend: { + condition: isConnected, + style: { + backgroundColor: utils.colors.textSecondaryHighlight, + }, + }, +})); + +export const ConnectionDot = ({ isConnected }: ConnectionDotProps) => { + const { applyStyle } = useNativeStyles(); + + return ; +}; diff --git a/suite-native/device-manager/src/components/DeviceItem/DeviceItem.tsx b/suite-native/device-manager/src/components/DeviceItem/DeviceItem.tsx new file mode 100644 index 00000000000..6d956f109cf --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/DeviceItem.tsx @@ -0,0 +1,33 @@ +import { Pressable } from 'react-native'; + +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { HStack } from '@suite-native/atoms'; +import { Icon } from '@suite-common/icons'; +import { TrezorDevice } from '@suite-common/suite-types'; + +import { DeviceItemContent } from './DeviceItemContent'; + +type DeviceItemProps = { + deviceState: NonNullable; + onPress: () => void; +}; + +const deviceItemWrapperStyle = prepareNativeStyle(utils => ({ + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: utils.spacings.medium, +})); + +export const DeviceItem = ({ deviceState, onPress }: DeviceItemProps) => { + const { applyStyle } = useNativeStyles(); + + return ( + + + + + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceItem/DeviceItemContent.tsx b/suite-native/device-manager/src/components/DeviceItem/DeviceItemContent.tsx new file mode 100644 index 00000000000..b8a2f8baee1 --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/DeviceItemContent.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { HStack, Box } from '@suite-native/atoms'; +import { Translation, useTranslate } from '@suite-native/intl'; +import { + selectDeviceByState, + DeviceRootState, + PORTFOLIO_TRACKER_DEVICE_ID, + selectAreAllDevicesDisconnectedOrAccountless, +} from '@suite-common/wallet-core'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { TypographyStyle } from '@trezor/theme'; +import { TrezorDevice } from '@suite-common/suite-types'; + +import { DeviceItemIcon } from './DeviceItemIcon'; +import { SimpleDeviceItemContent } from './SimpleDeviceItemContent'; +import { WalletDetailDeviceItemContent } from './WalletDetailDeviceItemContent'; + +export type DeviceItemContentVariant = 'simple' | 'walletDetail'; +export type DeviceItemContentMode = 'compact' | 'header'; + +export type DeviceItemContentProps = { + deviceState: NonNullable; + isPortfolioLabelDisplayed?: boolean; + headerTextVariant?: TypographyStyle; + variant?: DeviceItemContentVariant; + isCompact?: boolean; +}; + +const contentWrapperStyle = prepareNativeStyle<{ height: number }>((utils, { height }) => ({ + flexShrink: 1, + height, + alignItems: 'center', + spacing: utils.spacings.medium, +})); +const itemStyle = prepareNativeStyle(_ => ({ + flexShrink: 1, +})); + +export const DeviceItemContent = ({ + deviceState, + isPortfolioLabelDisplayed = true, + headerTextVariant = 'body', + variant = 'simple', + isCompact = true, +}: DeviceItemContentProps) => { + const { translate } = useTranslate(); + const { applyStyle } = useNativeStyles(); + + const device = useSelector((state: DeviceRootState) => selectDeviceByState(state, deviceState)); + const areAllDevicesDisconnectedOrAccountless = useSelector( + selectAreAllDevicesDisconnectedOrAccountless, + ); + + const isPortfolioTrackerDevice = device?.id === PORTFOLIO_TRACKER_DEVICE_ID; + + const deviceHeader = + (isPortfolioTrackerDevice ? device?.name : device?.label) ?? + translate('deviceManager.defaultHeader'); + + const walletNameLabel = device?.useEmptyPassphrase ? ( + + ) : ( + + ); + + if (!device) { + return null; + } + + return ( + + + + {variant === 'simple' ? ( + + ) : ( + + )} + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceItem/DeviceItemIcon.tsx b/suite-native/device-manager/src/components/DeviceItem/DeviceItemIcon.tsx new file mode 100644 index 00000000000..09f85890ab3 --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/DeviceItemIcon.tsx @@ -0,0 +1,31 @@ +import { useSelector } from 'react-redux'; + +import { Icon, IconSize } from '@suite-common/icons'; +import { + DeviceRootState, + PORTFOLIO_TRACKER_DEVICE_ID, + selectDeviceModelById, +} from '@suite-common/wallet-core'; +import { TrezorDevice } from '@suite-common/suite-types'; + +import { DeviceModelIcon } from '../DeviceModelIcon'; + +type DeviceItemIconProps = { + deviceId: TrezorDevice['id']; + iconSize: IconSize; +}; + +export const DeviceItemIcon = ({ deviceId, iconSize }: DeviceItemIconProps) => { + const deviceModel = useSelector((state: DeviceRootState) => + selectDeviceModelById(state, deviceId), + ); + + if (deviceId === PORTFOLIO_TRACKER_DEVICE_ID) { + return ; + } + if (deviceModel !== null) { + return ; + } + + return ; +}; diff --git a/suite-native/device-manager/src/components/DeviceItem/SimpleDeviceItemContent.tsx b/suite-native/device-manager/src/components/DeviceItem/SimpleDeviceItemContent.tsx new file mode 100644 index 00000000000..6a9e6d11e0a --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/SimpleDeviceItemContent.tsx @@ -0,0 +1,95 @@ +import { useSelector } from 'react-redux'; +import { ReactNode } from 'react'; + +import { HStack, Text, Box } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { + DeviceRootState, + selectAreAllDevicesDisconnectedOrAccountless, + selectDeviceByState, +} from '@suite-common/wallet-core'; +import { TypographyStyle } from '@trezor/theme'; +import { TrezorDevice } from '@suite-common/suite-types'; + +import { ConnectionDot } from './ConnectionDot'; + +export type SimpleDeviceItemContentProps = { + deviceState: NonNullable; + headerTextVariant?: TypographyStyle; + header: ReactNode; + isPortfolioLabelDisplayed?: boolean; + isPortfolioTrackerDevice: boolean; +}; + +const headerStyle = prepareNativeStyle(_ => ({ + flexShrink: 1, + overflow: 'visible', +})); + +export const SimpleDeviceItemContent = ({ + deviceState, + headerTextVariant, + isPortfolioLabelDisplayed, + header, + isPortfolioTrackerDevice, +}: SimpleDeviceItemContentProps) => { + const { applyStyle } = useNativeStyles(); + const device = useSelector((state: DeviceRootState) => selectDeviceByState(state, deviceState)); + const areAllDevicesDisconnectedOrAccountless = useSelector( + selectAreAllDevicesDisconnectedOrAccountless, + ); + + if (!device) { + return null; + } + + const isPortfolioTrackerSubHeaderVisible = + isPortfolioTrackerDevice && + isPortfolioLabelDisplayed && + !areAllDevicesDisconnectedOrAccountless; + + const isConnectionStateVisible = + !isPortfolioTrackerDevice && !areAllDevicesDisconnectedOrAccountless; + + return ( + <> + + {areAllDevicesDisconnectedOrAccountless ? ( + + ) : ( + header + )} + + + {isPortfolioTrackerSubHeaderVisible && ( + + + + )} + {isConnectionStateVisible && ( + + + + + + + )} + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceItem/WalletDetailDeviceItemContent.tsx b/suite-native/device-manager/src/components/DeviceItem/WalletDetailDeviceItemContent.tsx new file mode 100644 index 00000000000..8d05051753f --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceItem/WalletDetailDeviceItemContent.tsx @@ -0,0 +1,77 @@ +import { useSelector } from 'react-redux'; +import { ReactNode } from 'react'; + +import { HStack, Text, Box } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { DeviceRootState, selectDeviceByState } from '@suite-common/wallet-core'; +import { TypographyStyle } from '@trezor/theme'; +import { TrezorDevice } from '@suite-common/suite-types/src/device'; + +import { ConnectionDot } from './ConnectionDot'; + +export type WalletDetailDeviceItemContentProps = { + deviceState: NonNullable; + headerTextVariant?: TypographyStyle; + header: ReactNode; + subHeader?: ReactNode; + isPortfolioLabelDisplayed?: boolean; + isPortfolioTrackerDevice: boolean; +}; + +const headerStyle = prepareNativeStyle(utils => ({ + flexShrink: 1, + paddingRight: utils.spacings.small, + alignItems: 'center', + gap: utils.spacings.small, +})); + +const headerTextStyle = prepareNativeStyle(() => ({ + flexShrink: 1, +})); + +export const WalletDetailDeviceItemContent = ({ + deviceState, + headerTextVariant, + isPortfolioLabelDisplayed, + header, + subHeader, + isPortfolioTrackerDevice, +}: WalletDetailDeviceItemContentProps) => { + const { applyStyle } = useNativeStyles(); + const device = useSelector((state: DeviceRootState) => selectDeviceByState(state, deviceState)); + + if (!device) { + return null; + } + + return ( + <> + + + {header} + + {!isPortfolioTrackerDevice && } + + + {isPortfolioTrackerDevice && isPortfolioLabelDisplayed && ( + + + + )} + {!isPortfolioTrackerDevice && ( + + + {subHeader} + + + )} + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceItemContent.tsx b/suite-native/device-manager/src/components/DeviceItemContent.tsx deleted file mode 100644 index 4f3bb14bb85..00000000000 --- a/suite-native/device-manager/src/components/DeviceItemContent.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; - -import { Icon, IconName } from '@suite-common/icons'; -import { HStack, Box, Text } from '@suite-native/atoms'; -import { Translation } from '@suite-native/intl'; -import { - selectDeviceNameById, - DeviceRootState, - PORTFOLIO_TRACKER_DEVICE_ID, - selectDeviceLabelById, -} from '@suite-common/wallet-core'; -import { TrezorDevice } from '@suite-common/suite-types'; -import { TypographyStyle } from '@trezor/theme'; -import { useActiveColorScheme } from '@suite-native/theme'; - -type DeviceItemContentProps = { - deviceId?: TrezorDevice['id']; - isPortfolioLabelDisplayed?: boolean; - headerTextVariant?: TypographyStyle; -}; - -type DeviceItemIconProps = Pick; - -const DeviceItemIcon = ({ deviceId }: DeviceItemIconProps) => { - const activeColorScheme = useActiveColorScheme(); - - // TODO: when we enable remember mode, icon representing disconnected device have to be handled. - const connectedDeviceIcon: IconName = - activeColorScheme === 'standard' ? 'trezorConnectedLight' : 'trezorConnectedDark'; - - switch (deviceId) { - case undefined: - return ; - case PORTFOLIO_TRACKER_DEVICE_ID: - return ; - default: - return ; - } -}; - -export const DeviceItemContent = ({ - deviceId, - isPortfolioLabelDisplayed = true, - headerTextVariant = 'body', -}: DeviceItemContentProps) => { - const deviceName = useSelector((state: DeviceRootState) => - selectDeviceNameById(state, deviceId), - ); - const deviceLabel = useSelector((state: DeviceRootState) => - selectDeviceLabelById(state, deviceId), - ); - - const isPortfolioTrackerDevice = deviceId === PORTFOLIO_TRACKER_DEVICE_ID; - - const deviceHeader = (isPortfolioTrackerDevice ? deviceName : deviceLabel) ?? ( - - ); - - return ( - - - - {deviceHeader} - {deviceId && ( - - {isPortfolioTrackerDevice ? ( - isPortfolioLabelDisplayed && ( - - - - ) - ) : ( - // TODO: when we enable remember mode, grey 'Disconnected' label has to be displayed. - - - - )} - - )} - - - ); -}; diff --git a/suite-native/device-manager/src/components/DeviceList.tsx b/suite-native/device-manager/src/components/DeviceList.tsx new file mode 100644 index 00000000000..54ee40aeda7 --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceList.tsx @@ -0,0 +1,181 @@ +import { useSelector } from 'react-redux'; +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withDelay, + withTiming, +} from 'react-native-reanimated'; +import { useEffect, useState } from 'react'; + +import { useNavigation } from '@react-navigation/native'; +import { A } from '@mobily/ts-belt'; + +import { + ConnectDeviceStackRoutes, + RootStackParamList, + RootStackRoutes, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; +import { analytics, EventType } from '@suite-native/analytics'; +import { selectDevice, selectInstacelessUnselectedDevices } from '@suite-common/wallet-core'; +import { Button, VStack, Box, TextDivider } from '@suite-native/atoms'; +import { TrezorDevice } from '@suite-common/suite-types'; +import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { DeviceItem } from './DeviceItem/DeviceItem'; +import { useDeviceManager } from '../hooks/useDeviceManager'; +import { MANAGER_MODAL_BOTTOM_RADIUS } from './DeviceManagerModal'; + +type NavigationProp = StackToStackCompositeNavigationProps< + RootStackParamList, + RootStackRoutes.AppTabs, + RootStackParamList +>; + +type DeviceListProps = { + isVisible: boolean; + onSelectDevice: (device: TrezorDevice) => void; +}; + +const ITEM_HEIGHT = 66; +const BUTTON_HEIGHT = 48; +const VERTICAL_PADDING = 16; +const SEPARATOR_VERTICAL_PADDING = 4; +const SEPARATOR_HEIGHT = 26; +const TOP_LIST_PADDING = 12; + +const buttonWrapperStyle = prepareNativeStyle(utils => ({ + paddingHorizontal: utils.spacings.medium, +})); + +const listStaticStyle = prepareNativeStyle(utils => ({ + backgroundColor: utils.colors.backgroundSurfaceElevation1, + borderBottomLeftRadius: MANAGER_MODAL_BOTTOM_RADIUS, + borderBottomRightRadius: MANAGER_MODAL_BOTTOM_RADIUS, + borderWidth: utils.borders.widths.small, + borderColor: utils.colors.borderElevation0, + marginTop: -MANAGER_MODAL_BOTTOM_RADIUS, + marginBottom: -MANAGER_MODAL_BOTTOM_RADIUS, + paddingTop: MANAGER_MODAL_BOTTOM_RADIUS + TOP_LIST_PADDING, + paddingBottom: VERTICAL_PADDING, + zIndex: 10, + ...utils.boxShadows.small, +})); + +export const DeviceList = ({ isVisible, onSelectDevice }: DeviceListProps) => { + const { applyStyle, utils } = useNativeStyles(); + const navigation = useNavigation(); + const { setIsDeviceManagerVisible } = useDeviceManager(); + const device = useSelector(selectDevice); + const notSelectedInstancelessDevices = useSelector(selectInstacelessUnselectedDevices); + const opacity = useSharedValue(0); + const height = useSharedValue(0); + const [isShown, setIsShown] = useState(false); + + const handleConnectDevice = () => { + if (device) { + onSelectDevice(device); + } + setIsDeviceManagerVisible(false); + navigation.navigate(RootStackRoutes.ConnectDeviceStack, { + screen: ConnectDeviceStackRoutes.ConnectAndUnlockDevice, + }); + analytics.report({ + type: EventType.DeviceManagerClick, + payload: { action: 'connectDeviceButton' }, + }); + }; + + //to hide/show the list with animation + useEffect(() => { + if (isVisible) { + const hasNotSelectedInstancelessDevices = notSelectedInstancelessDevices.length > 0; + + const paddingsHeight = VERTICAL_PADDING * 2; + + const otherDevicesHeight = notSelectedInstancelessDevices.length * ITEM_HEIGHT; + + const separatorHeight = hasNotSelectedInstancelessDevices ? SEPARATOR_HEIGHT : 0; + + const separatorPaddding = hasNotSelectedInstancelessDevices + ? SEPARATOR_VERTICAL_PADDING * 2 + : 0; + + const radiusesHeight = MANAGER_MODAL_BOTTOM_RADIUS * 2; + + const h = + otherDevicesHeight + + radiusesHeight + + separatorHeight + + separatorPaddding + + BUTTON_HEIGHT + + paddingsHeight; + + opacity.value = withDelay(100, withTiming(1, { duration: 200 })); + height.value = withTiming(h, { duration: 300 }); + + setIsShown(true); + } else { + opacity.value = withTiming(0, { duration: 300 }); + height.value = withDelay( + 100, + withTiming(0, { duration: 200 }, () => { + runOnJS(setIsShown)(false); + }), + ); + } + }, [height, isVisible, opacity, notSelectedInstancelessDevices.length, utils.spacings.small]); + + const listAnimatedStyle = useAnimatedStyle( + () => ({ + opacity: opacity.value, + height: height.value, + }), + [], + ); + + if (!isShown) { + return null; + } + + return ( + + + + {notSelectedInstancelessDevices.map(d => { + if (d.state === undefined) { + return null; + } + + return ( + onSelectDevice(d)} + /> + ); + })} + + + {A.isEmpty(notSelectedInstancelessDevices) ? ( + + + + ) : ( + <> + + + + + + )} + + + ); +}; diff --git a/suite-native/device-manager/src/components/DeviceManagerContent.tsx b/suite-native/device-manager/src/components/DeviceManagerContent.tsx index aa62db94171..ffd23bfe920 100644 --- a/suite-native/device-manager/src/components/DeviceManagerContent.tsx +++ b/suite-native/device-manager/src/components/DeviceManagerContent.tsx @@ -1,90 +1,98 @@ -import { useSelector } from 'react-redux'; -import { useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useState } from 'react'; -import { A } from '@mobily/ts-belt'; -import { useNavigation } from '@react-navigation/native'; - -import { analytics, EventType } from '@suite-native/analytics'; -import { Button, Text, VStack } from '@suite-native/atoms'; +import { HStack, VStack } from '@suite-native/atoms'; import { - selectDevices, + PORTFOLIO_TRACKER_DEVICE_ID, + selectDevice, + selectDeviceThunk, + selectIsDeviceDiscoveryActive, selectIsPortfolioTrackerDevice, - selectDeviceId, - selectIsNoPhysicalDeviceConnected, } from '@suite-common/wallet-core'; -import { - ConnectDeviceStackRoutes, - RootStackParamList, - RootStackRoutes, - StackToStackCompositeNavigationProps, -} from '@suite-native/navigation'; -import { Translation } from '@suite-native/intl'; import { FeatureFlag, useFeatureFlag } from '@suite-native/feature-flags'; +import { EventType, analytics } from '@suite-native/analytics'; +import { TrezorDevice } from '@suite-common/suite-types'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { AddHiddenWalletButton } from './AddHiddenWalletButton'; +import { DeviceList } from './DeviceList'; +import { DeviceInfoButton } from './DeviceInfoButton'; import { DeviceManagerModal } from './DeviceManagerModal'; -import { DeviceItem } from './DeviceItem'; -import { DeviceControlButtons } from './DeviceControlButtons'; +import { DevicesToggleButton } from './DevicesToggleButton'; +import { WalletList } from './WalletList'; import { useDeviceManager } from '../hooks/useDeviceManager'; -import { AddHiddenWalletButton } from './AddHiddenWalletButton'; -type NavigationProp = StackToStackCompositeNavigationProps< - RootStackParamList, - RootStackRoutes.AppTabs, - RootStackParamList ->; +const deviceButtonsStyle = prepareNativeStyle(utils => ({ + width: '100%', + paddingHorizontal: utils.spacings.medium, + paddingBottom: utils.spacings.medium, +})); export const DeviceManagerContent = () => { - const navigation = useNavigation(); + const { applyStyle } = useNativeStyles(); + const [isChangeDeviceRequested, setIsChangeDeviceRequested] = useState(false); - const devices = useSelector(selectDevices); - const selectedDeviceId = useSelector(selectDeviceId); const isPortfolioTrackerDevice = useSelector(selectIsPortfolioTrackerDevice); - const isNoPhysicalDeviceConnected = useSelector(selectIsNoPhysicalDeviceConnected); + const [isPassphraseFeatureEnabled] = useFeatureFlag(FeatureFlag.IsPassphraseEnabled); + const isDiscoveryActive = useSelector(selectIsDeviceDiscoveryActive); + const device = useSelector(selectDevice); const { setIsDeviceManagerVisible } = useDeviceManager(); - const [isPassphraseFeatureEnabled] = useFeatureFlag(FeatureFlag.IsPassphraseEnabled); + const toggleIsChangeDeviceRequested = () => + setIsChangeDeviceRequested(!isChangeDeviceRequested); + const dispatch = useDispatch(); - const handleConnectDevice = () => { + const handleSelectDevice = (selectedDevice: TrezorDevice) => { + dispatch(selectDeviceThunk(selectedDevice)); + setIsChangeDeviceRequested(false); setIsDeviceManagerVisible(false); - navigation.navigate(RootStackRoutes.ConnectDeviceStack, { - screen: ConnectDeviceStackRoutes.ConnectAndUnlockDevice, - }); + analytics.report({ type: EventType.DeviceManagerClick, - payload: { action: 'connectDeviceButton' }, + payload: { + action: + selectedDevice.id === PORTFOLIO_TRACKER_DEVICE_ID + ? 'portfolioTracker' + : 'connectDeviceButton', + }, }); }; - const listedDevice = useMemo( - () => devices.filter(device => device.id !== selectedDeviceId), - [devices, selectedDeviceId], - ); + if (!device) { + return null; + } + + const isAddHiddenWalletButtonVisible = + isPassphraseFeatureEnabled && !isDiscoveryActive && device?.connected; return ( - - {!isPortfolioTrackerDevice && } - {!isPortfolioTrackerDevice && isPassphraseFeatureEnabled && } - {A.isNotEmpty(listedDevice) && ( - - - - - {listedDevice.map(device => ( - - ))} - - )} - {isNoPhysicalDeviceConnected && ( - - - - - - - )} + + ) + } + onClose={() => setIsChangeDeviceRequested(false)} + > + + + {!isPortfolioTrackerDevice && ( + <> + + + + {isAddHiddenWalletButtonVisible && } + + + )} + ); }; diff --git a/suite-native/device-manager/src/components/DeviceManagerModal.tsx b/suite-native/device-manager/src/components/DeviceManagerModal.tsx index c1aa581c68f..63ccd01bb49 100644 --- a/suite-native/device-manager/src/components/DeviceManagerModal.tsx +++ b/suite-native/device-manager/src/components/DeviceManagerModal.tsx @@ -1,47 +1,62 @@ import { GestureResponderEvent, Modal, Pressable } from 'react-native'; import { ReactNode } from 'react'; import { useSafeAreaInsets, EdgeInsets } from 'react-native-safe-area-context'; -import Animated, { SlideInUp } from 'react-native-reanimated'; +import Animated, { FadeIn, SlideInUp } from 'react-native-reanimated'; +import { useSelector } from 'react-redux'; -import { ScreenHeaderWrapper, VStack } from '@suite-native/atoms'; +import { ScreenHeaderWrapper, Box, HStack } from '@suite-native/atoms'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { selectDeviceState } from '@suite-common/wallet-core'; -import { DeviceSwitch } from './DeviceSwitch'; import { useDeviceManager } from '../hooks/useDeviceManager'; +import { DeviceItemContent } from './DeviceItem/DeviceItemContent'; type DeviceManagerModalProps = { children: ReactNode; + customSwitchRightView?: ReactNode; + onClose?: () => void; }; -const MANAGER_MODAL_BOTTOM_RADIUS = 20; +export const MANAGER_MODAL_BOTTOM_RADIUS = 12; const modalBackgroundOverlayStyle = prepareNativeStyle(utils => ({ flex: 1, backgroundColor: utils.transparentize(0.3, utils.colors.backgroundNeutralBold), })); -const deviceManagerModalWrapperStyle = prepareNativeStyle<{ insets: EdgeInsets }>( +const deviceManagerModalWrapperStyle = prepareNativeStyle(utils => ({ + backgroundColor: utils.colors.backgroundSurfaceElevation0, + borderBottomLeftRadius: MANAGER_MODAL_BOTTOM_RADIUS, + borderBottomRightRadius: MANAGER_MODAL_BOTTOM_RADIUS, +})); + +const deviceSwitchWrapperStyle = prepareNativeStyle<{ insets: EdgeInsets }>( (utils, { insets }) => ({ - paddingTop: Math.max(insets.top, utils.spacings.small), + paddingTop: insets.top + utils.spacings.large, backgroundColor: utils.colors.backgroundSurfaceElevation1, borderBottomLeftRadius: MANAGER_MODAL_BOTTOM_RADIUS, borderBottomRightRadius: MANAGER_MODAL_BOTTOM_RADIUS, + borderWidth: utils.borders.widths.small, + borderColor: utils.colors.borderElevation0, + zIndex: 20, + ...utils.boxShadows.small, }), ); -const contentWrapperStyle = prepareNativeStyle(utils => ({ - paddingHorizontal: utils.spacings.medium, - paddingBottom: utils.spacings.medium, -})); - -export const DeviceManagerModal = ({ children }: DeviceManagerModalProps) => { +export const DeviceManagerModal = ({ + children, + customSwitchRightView, + onClose, +}: DeviceManagerModalProps) => { const { applyStyle } = useNativeStyles(); + const deviceState = useSelector(selectDeviceState); const insets = useSafeAreaInsets(); const { setIsDeviceManagerVisible, isDeviceManagerVisible } = useDeviceManager(); const handleClose = () => { + onClose?.(); setIsDeviceManagerVisible(false); }; @@ -56,18 +71,36 @@ export const DeviceManagerModal = ({ children }: DeviceManagerModalProps) => { visible={isDeviceManagerVisible} presentationStyle="overFullScreen" animationType="fade" + statusBarTranslucent={true} > - - - - - {children} - + + + + + {deviceState && ( + + )} + + {customSwitchRightView} + + + + {children} diff --git a/suite-native/device-manager/src/components/DeviceModelIcon.tsx b/suite-native/device-manager/src/components/DeviceModelIcon.tsx new file mode 100644 index 00000000000..eb1284b5eaa --- /dev/null +++ b/suite-native/device-manager/src/components/DeviceModelIcon.tsx @@ -0,0 +1,18 @@ +import { Icon, IconName, IconSize } from '@suite-common/icons'; +import { DeviceModelInternal } from '@trezor/connect'; + +type DeviceModelIconProps = { + deviceModel: DeviceModelInternal; + size?: IconSize | number; +}; + +const icons = { + T1B1: 'trezorT1B1', + T2T1: 'trezorT2T1', + T2B1: 'trezorT2B1', + T3T1: 'trezorT3T1', +} as const satisfies Record; + +export const DeviceModelIcon = ({ deviceModel, size }: DeviceModelIconProps) => ( + +); diff --git a/suite-native/device-manager/src/components/DeviceSwitch.tsx b/suite-native/device-manager/src/components/DeviceSwitch.tsx index 38b2e2939c5..a6238104d53 100644 --- a/suite-native/device-manager/src/components/DeviceSwitch.tsx +++ b/suite-native/device-manager/src/components/DeviceSwitch.tsx @@ -1,17 +1,14 @@ -import { Pressable, TouchableOpacity } from 'react-native'; +import { Pressable } from 'react-native'; import { useSelector } from 'react-redux'; import { Box, HStack } from '@suite-native/atoms'; import { Icon } from '@suite-common/icons'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { - selectDeviceId, - selectAreAllDevicesDisconnectedOrAccountless, -} from '@suite-common/wallet-core'; +import { selectDeviceInstances, selectDeviceState } from '@suite-common/wallet-core'; import { SCREEN_HEADER_HEIGHT } from '../constants'; import { useDeviceManager } from '../hooks/useDeviceManager'; -import { DeviceItemContent } from './DeviceItemContent'; +import { DeviceItemContent } from './DeviceItem/DeviceItemContent'; type SwitchStyleProps = { isDeviceManagerVisible: boolean }; @@ -43,10 +40,8 @@ const switchWrapperStyle = prepareNativeStyle(_ => ({ export const DeviceSwitch = () => { const { applyStyle } = useNativeStyles(); - const areAllDevicesDisconnectedOrAccountless = useSelector( - selectAreAllDevicesDisconnectedOrAccountless, - ); - const deviceId = useSelector(selectDeviceId); + const deviceState = useSelector(selectDeviceState); + const wallets = useSelector(selectDeviceInstances); const { setIsDeviceManagerVisible, isDeviceManagerVisible } = useDeviceManager(); @@ -58,26 +53,16 @@ export const DeviceSwitch = () => { - - {!isDeviceManagerVisible && ( - + {deviceState && ( + 1 ? 'walletDetail' : 'simple'} + /> )} + - {isDeviceManagerVisible && ( - - - - - - )} ); diff --git a/suite-native/device-manager/src/components/DevicesToggleButton.tsx b/suite-native/device-manager/src/components/DevicesToggleButton.tsx new file mode 100644 index 00000000000..aabc81bf1ad --- /dev/null +++ b/suite-native/device-manager/src/components/DevicesToggleButton.tsx @@ -0,0 +1,26 @@ +import { Button, Text } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; + +type DeviceButtonState = 'open' | 'closed'; + +type DevicesToggleButtonProps = { + deviceButtonState: DeviceButtonState; + onDeviceButtonTap: () => void; +}; + +export const DevicesToggleButton = ({ + deviceButtonState = 'closed', + onDeviceButtonTap, +}: DevicesToggleButtonProps) => ( + +); diff --git a/suite-native/device-manager/src/components/PortfolioTrackerDeviceManagerContent.tsx b/suite-native/device-manager/src/components/PortfolioTrackerDeviceManagerContent.tsx index e6f95ff7b01..dc894fe9da3 100644 --- a/suite-native/device-manager/src/components/PortfolioTrackerDeviceManagerContent.tsx +++ b/suite-native/device-manager/src/components/PortfolioTrackerDeviceManagerContent.tsx @@ -13,6 +13,7 @@ import { } from '@suite-native/navigation'; import { selectIsDeviceDiscoveryEmpty } from '@suite-common/wallet-core'; import { useOpenLink } from '@suite-native/link'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { DeviceManagerModal } from './DeviceManagerModal'; import { useDeviceManager } from '../hooks/useDeviceManager'; @@ -23,8 +24,18 @@ type NavigationProp = StackToStackCompositeNavigationProps< RootStackParamList >; +const contentStyle = prepareNativeStyle(utils => ({ + paddingHorizontal: utils.spacings.medium, + + spacing: utils.spacings.medium, + + paddingBottom: utils.spacings.medium, + marginTop: utils.spacings.medium, +})); + export const PortfolioTrackerDeviceManagerContent = () => { const openLink = useOpenLink(); + const { applyStyle } = useNativeStyles(); const isDeviceDiscoveryEmpty = useSelector(selectIsDeviceDiscoveryEmpty); @@ -71,27 +82,29 @@ export const PortfolioTrackerDeviceManagerContent = () => { return ( - - - - - - - + + + + + + + ); diff --git a/suite-native/device-manager/src/components/WalletItem.tsx b/suite-native/device-manager/src/components/WalletItem.tsx new file mode 100644 index 00000000000..85cf54249c5 --- /dev/null +++ b/suite-native/device-manager/src/components/WalletItem.tsx @@ -0,0 +1,76 @@ +import { Pressable } from 'react-native'; +import { useSelector } from 'react-redux'; + +import { HStack, Radio, Text } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { Icon } from '@suite-common/icons'; +import { TrezorDevice } from '@suite-common/suite-types'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { selectDevice, selectDeviceByState } from '@suite-common/wallet-core'; + +type WalletItemProps = { + deviceState: NonNullable; + onPress: () => void; + isSelectable?: boolean; +}; + +const walletItemStyle = prepareNativeStyle<{ isSelected: boolean }>((utils, { isSelected }) => ({ + paddingHorizontal: utils.spacings.medium, + justifyContent: 'space-between', + alignItems: 'center', + height: 60, + gap: 12, + backgroundColor: utils.colors.backgroundSurfaceElevation1, + borderWidth: utils.borders.widths.small, + borderRadius: 12, + borderColor: utils.colors.borderElevation1, + extend: { + condition: isSelected, + style: { + borderWidth: utils.borders.widths.large, + borderColor: utils.colors.borderSecondary, + }, + }, +})); + +export const WalletItem = ({ deviceState, onPress, isSelectable = true }: WalletItemProps) => { + const { applyStyle } = useNativeStyles(); + const device = useSelector((state: any) => selectDeviceByState(state, deviceState)); + const selectedDevice = useSelector(selectDevice); + + if (!device) { + return null; + } + + const walletNameLabel = device.useEmptyPassphrase ? ( + + ) : ( + + ); + + const isSelected = + selectedDevice?.id === device.id && selectedDevice?.instance === device.instance; + + const showAsSelected = isSelected && isSelectable; + + return ( + + + + + {walletNameLabel} + + {isSelectable && } + + + ); +}; diff --git a/suite-native/device-manager/src/components/WalletList.tsx b/suite-native/device-manager/src/components/WalletList.tsx new file mode 100644 index 00000000000..21a5cd122d5 --- /dev/null +++ b/suite-native/device-manager/src/components/WalletList.tsx @@ -0,0 +1,34 @@ +import { useSelector } from 'react-redux'; + +import { selectDeviceInstances } from '@suite-common/wallet-core'; +import { VStack } from '@suite-native/atoms'; +import { TrezorDevice } from '@suite-common/suite-types'; + +import { WalletItem } from './WalletItem'; + +type WalletListProps = { + onSelectDevice: (device: TrezorDevice) => void; +}; + +export const WalletList = ({ onSelectDevice }: WalletListProps) => { + const devices = useSelector(selectDeviceInstances); + + return ( + + {devices.map(device => { + if (!device.state) { + return null; + } + + return ( + 1} + onPress={() => onSelectDevice(device)} + /> + ); + })} + + ); +}; diff --git a/suite-native/device-manager/src/index.ts b/suite-native/device-manager/src/index.ts index dd2e85001eb..28822510f7b 100644 --- a/suite-native/device-manager/src/index.ts +++ b/suite-native/device-manager/src/index.ts @@ -1,2 +1,3 @@ export * from './components/DeviceManager'; +export * from './components/DeviceModelIcon'; export * from './components/DeviceManagerScreenHeader'; diff --git a/suite-native/device-manager/tsconfig.json b/suite-native/device-manager/tsconfig.json index 4cc2d8793b5..d9ca9221060 100644 --- a/suite-native/device-manager/tsconfig.json +++ b/suite-native/device-manager/tsconfig.json @@ -6,6 +6,9 @@ { "path": "../../suite-common/suite-types" }, + { + "path": "../../suite-common/suite-utils" + }, { "path": "../../suite-common/wallet-core" }, diff --git a/suite-native/device/package.json b/suite-native/device/package.json index 088fdabc257..9150e0ac633 100644 --- a/suite-native/device/package.json +++ b/suite-native/device/package.json @@ -13,7 +13,9 @@ "@mobily/ts-belt": "^3.13.1", "@react-navigation/native": "6.1.10", "@reduxjs/toolkit": "1.9.5", + "@suite-common/icons": "workspace:*", "@suite-common/redux-utils": "workspace:*", + "@suite-common/suite-types": "workspace:*", "@suite-common/wallet-core": "workspace:*", "@suite-common/wallet-types": "workspace:*", "@suite-native/alerts": "workspace:*", diff --git a/suite-native/device/tsconfig.json b/suite-native/device/tsconfig.json index 9e357b6dc88..5b2f99fc5e6 100644 --- a/suite-native/device/tsconfig.json +++ b/suite-native/device/tsconfig.json @@ -2,9 +2,15 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "libDev" }, "references": [ + { + "path": "../../suite-common/icons" + }, { "path": "../../suite-common/redux-utils" }, + { + "path": "../../suite-common/suite-types" + }, { "path": "../../suite-common/wallet-core" }, diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index 5b0a7b5b7c5..41ec2ddfe0c 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -588,16 +588,13 @@ export const en = { }, deviceManager: { deviceButtons: { - eject: 'Eject', deviceInfo: 'Device info', - addHiddenWallet: 'Add hidden wallet', - }, - deviceList: { - sectionTitle: 'Open', + addHiddenWallet: 'Open passphrase', + devices: 'Change', }, - connectDevice: { - sectionTitle: 'Connect Trezor device', - connectButton: 'Connect', + connectButton: { + another: 'Connect another device', + first: 'Connect your device', }, portfolioTracker: { explore: 'Explore Trezor', @@ -605,14 +602,20 @@ export const en = { exploreShop: 'Explore Trezor Shop', }, status: { - portfolioTracker: 'Sync & track coins', + portfolioTracker: 'Track your coins without Trezor', connected: 'Connected', + disconnected: 'Disconnected', }, syncCoinsButton: { syncMyCoins: 'Sync my coins', syncAnother: 'Sync another coin', }, defaultHeader: 'Hi there!', + wallet: { + standard: 'Standard wallet', + portfolio: 'Portfolio tracker', + defaultPassphrase: 'Passphrase wallet #{index}', + }, }, deviceInfo: { installedFw: 'Installed firmware: {version}', diff --git a/suite-native/module-settings/src/components/TrezorModelIcon.tsx b/suite-native/module-settings/src/components/TrezorModelIcon.tsx deleted file mode 100644 index 4fdaa4beae1..00000000000 --- a/suite-native/module-settings/src/components/TrezorModelIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Icon, IconName } from '@suite-common/icons'; -import { AcquiredDevice } from '@suite-common/suite-types'; -import { DeviceModelInternal } from '@trezor/connect'; - -const icons = { - T1B1: 'trezorT1B1', - T2T1: 'trezorT2T1', - T2B1: 'trezorT2B1', - T3T1: 'trezorT3T1', -} as const satisfies Record; - -type TrezorModelIconProps = { device: AcquiredDevice }; - -export const TrezorModelIcon = ({ device }: TrezorModelIconProps) => { - const model = device.features?.internal_model as DeviceModelInternal; - const iconName = icons[model]; - if (!iconName) return null; - - return ; -}; diff --git a/suite-native/module-settings/src/components/ViewOnly/DevicesManagement.tsx b/suite-native/module-settings/src/components/ViewOnly/DevicesManagement.tsx index b9d8f4c8a95..9819169cef6 100644 --- a/suite-native/module-settings/src/components/ViewOnly/DevicesManagement.tsx +++ b/suite-native/module-settings/src/components/ViewOnly/DevicesManagement.tsx @@ -1,31 +1,14 @@ import { useSelector } from 'react-redux'; import { Box, Card, Divider, HStack, Text } from '@suite-native/atoms'; -import { Translation, TxKeyPath } from '@suite-native/intl'; +import { Translation } from '@suite-native/intl'; +import { DeviceModelIcon } from '@suite-native/device-manager'; import { selectPhysicalDevicesGrouppedById } from '@suite-common/wallet-core'; -import { Icon, IconName } from '@suite-common/icons'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Color } from '@trezor/theme'; import { About, AboutProps } from './About'; -import { TrezorModelIcon } from './TrezorModelIcon'; import { WalletRow } from './WalletRow'; -type ConnectionStyle = { color: Color; iconName: IconName; translationKey: TxKeyPath }; - -const connectionStyleMap = { - connected: { - color: 'textSecondaryHighlight', - iconName: 'linkChain', - translationKey: 'moduleSettings.viewOnly.connected', - }, - disconnected: { - color: 'textSubdued', - iconName: 'linkChainBroken', - translationKey: 'moduleSettings.viewOnly.disconnected', - }, -} as const satisfies Record; - const cardStyle = prepareNativeStyle(utils => ({ padding: 0, marginTop: utils.spacings.large, @@ -37,6 +20,13 @@ const deviceStyle = prepareNativeStyle(utils => ({ gap: 12, })); +const dotStyle = prepareNativeStyle<{ isConnected: boolean }>((utils, { isConnected }) => ({ + width: utils.spacings.small, + height: utils.spacings.small, + borderRadius: utils.borders.radii.round, + backgroundColor: isConnected ? utils.colors.textSecondaryHighlight : utils.colors.textSubdued, +})); + export const DevicesManagement = ({ onPressAbout }: AboutProps) => { const deviceGroups = useSelector(selectPhysicalDevicesGrouppedById); const { applyStyle } = useNativeStyles(); @@ -48,25 +38,39 @@ export const DevicesManagement = ({ onPressAbout }: AboutProps) => { {deviceGroups.map(devices => { const [firstDevice] = devices; - const connectionStyle = - connectionStyleMap[firstDevice.connected ? 'connected' : 'disconnected']; + const deviceModel = firstDevice.features?.internal_model; return ( - + {deviceModel && ( + + )} {firstDevice.label} - - + - - + + diff --git a/suite-native/module-settings/src/components/ViewOnly/TrezorModelIcon.tsx b/suite-native/module-settings/src/components/ViewOnly/TrezorModelIcon.tsx deleted file mode 100644 index 4fdaa4beae1..00000000000 --- a/suite-native/module-settings/src/components/ViewOnly/TrezorModelIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Icon, IconName } from '@suite-common/icons'; -import { AcquiredDevice } from '@suite-common/suite-types'; -import { DeviceModelInternal } from '@trezor/connect'; - -const icons = { - T1B1: 'trezorT1B1', - T2T1: 'trezorT2T1', - T2B1: 'trezorT2B1', - T3T1: 'trezorT3T1', -} as const satisfies Record; - -type TrezorModelIconProps = { device: AcquiredDevice }; - -export const TrezorModelIcon = ({ device }: TrezorModelIconProps) => { - const model = device.features?.internal_model as DeviceModelInternal; - const iconName = icons[model]; - if (!iconName) return null; - - return ; -}; diff --git a/suite-native/module-settings/src/components/ViewOnly/WalletRow.tsx b/suite-native/module-settings/src/components/ViewOnly/WalletRow.tsx index 24d341d820f..9bde3424a7e 100644 --- a/suite-native/module-settings/src/components/ViewOnly/WalletRow.tsx +++ b/suite-native/module-settings/src/components/ViewOnly/WalletRow.tsx @@ -6,7 +6,7 @@ import { deviceActions, toggleRememberDevice } from '@suite-common/wallet-core'; import { useAlert } from '@suite-native/alerts'; import { useToast } from '@suite-native/toasts'; import { Icon } from '@suite-common/icons'; -import { AcquiredDevice } from '@suite-common/suite-types'; +import { TrezorDevice } from '@suite-common/suite-types'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; const walletRowStyle = prepareNativeStyle(utils => ({ @@ -16,7 +16,7 @@ const walletRowStyle = prepareNativeStyle(utils => ({ height: 60, })); -export const WalletRow = ({ device }: { device: AcquiredDevice }) => { +export const WalletRow = ({ device }: { device: TrezorDevice }) => { const dispatch = useDispatch(); const { showAlert, hideAlert } = useAlert(); const { showToast } = useToast(); diff --git a/yarn.lock b/yarn.lock index b5ca83237c6..d2734b71b0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9059,6 +9059,7 @@ __metadata: "@reduxjs/toolkit": "npm:1.9.5" "@suite-common/icons": "workspace:*" "@suite-common/suite-types": "workspace:*" + "@suite-common/suite-utils": "workspace:*" "@suite-common/wallet-core": "workspace:*" "@suite-native/analytics": "workspace:*" "@suite-native/atoms": "workspace:*" @@ -9095,7 +9096,9 @@ __metadata: "@mobily/ts-belt": "npm:^3.13.1" "@react-navigation/native": "npm:6.1.10" "@reduxjs/toolkit": "npm:1.9.5" + "@suite-common/icons": "workspace:*" "@suite-common/redux-utils": "workspace:*" + "@suite-common/suite-types": "workspace:*" "@suite-common/wallet-core": "workspace:*" "@suite-common/wallet-types": "workspace:*" "@suite-native/alerts": "workspace:*"