feat(suite-native): device info detail (#9979)

This commit is contained in:
vytick
2023-11-19 15:07:08 +04:00
committed by GitHub
parent f5286f6136
commit 74faef71bd
17 changed files with 245 additions and 50 deletions

View File

@@ -1,21 +1,34 @@
import { FirmwareType, Device } from '@trezor/connect';
import { FirmwareType, Device, VersionArray } from '@trezor/connect';
import { isDeviceInBootloaderMode } from './modeUtils';
export const getFirmwareRevision = (device?: Device) => device?.features?.revision || '';
export const getFirmwareVersionArray = (device?: Device): VersionArray | null => {
if (!device?.features) {
return null;
}
const { features } = device;
if (isDeviceInBootloaderMode(device)) {
return features.fw_major && features.fw_minor && features.fw_patch
? ([features.fw_major, features.fw_minor, features.fw_patch] as VersionArray)
: null;
}
return [features.major_version, features.minor_version, features.patch_version];
};
export const getFirmwareVersion = (device?: Device) => {
if (!device?.features) {
return '';
}
const { features } = device;
if (isDeviceInBootloaderMode(device)) {
return features.fw_major
? `${features.fw_major}.${features.fw_minor}.${features.fw_patch}`
: '';
}
return `${features.major_version}.${features.minor_version}.${features.patch_version}`;
};

View File

@@ -77,6 +77,13 @@ export const removeButtonRequests = createAction(
}),
);
export const forgetAndDisconnectDevice = createAction(
`${MODULE_PREFIX}/forgetAndDisconnectDevice`,
(payload: TrezorDevice) => ({
payload,
}),
);
export const deviceActions = {
connectDevice,
connectUnacquiredDevice,
@@ -94,4 +101,5 @@ export const deviceActions = {
selectDevice,
updateSelectedDevice,
removeButtonRequests,
forgetAndDisconnectDevice,
};

View File

@@ -1,8 +1,10 @@
import { memoize } from 'proxy-memoize';
import * as deviceUtils from '@suite-common/suite-utils';
import { getStatus } from '@suite-common/suite-utils';
import { Device, Features, AuthenticateDeviceResult } from '@trezor/connect';
import { DiscoveryStatus } from '@suite-common/wallet-constants';
import { getFirmwareVersion } from '@trezor/device-utils';
import { getFirmwareVersion, getFirmwareVersionArray } from '@trezor/device-utils';
import { Network, networks } from '@suite-common/wallet-config';
import { versionUtils } from '@trezor/utils';
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
@@ -416,6 +418,11 @@ const forget = (draft: State, device: TrezorDevice) => {
}
};
const forgetAndDisconnect = (draft: State, device: TrezorDevice) => {
forget(draft, device);
disconnectDevice(draft, device);
};
const addButtonRequest = (
draft: State,
device: TrezorDevice | undefined,
@@ -498,7 +505,10 @@ export const prepareDeviceReducer = createReducerWithExtraDeps(initialState, (bu
setDeviceAuthenticity(state, payload.device, payload.result);
})
.addCase(extra.actionTypes.setDeviceMetadata, extra.reducers.setDeviceMetadataReducer)
.addCase(extra.actionTypes.storageLoad, extra.reducers.storageLoadDevices);
.addCase(extra.actionTypes.storageLoad, extra.reducers.storageLoadDevices)
.addCase(deviceActions.forgetAndDisconnectDevice, (state, { payload }) => {
forgetAndDisconnect(state, payload);
});
});
export const selectDevices = (state: DeviceRootState) => state.device?.devices;
@@ -687,13 +697,22 @@ export const selectSelectedDeviceName = (state: DeviceRootState) => {
return selectDeviceName(state, selectedDevice?.id);
};
export const selectDeviceFirmwareVersion = (state: DeviceRootState) => {
const device = selectDevice(state);
return device?.firmwareRelease?.release.version ?? null;
export const selectSelectedDeviceId = (state: DeviceRootState) => {
const selectedDevice = selectDevice(state);
return selectedDevice?.id ?? null;
};
export const selectDeviceModel = (state: DeviceRootState) => {
const features = selectDeviceFeatures(state);
return features?.internal_model ?? null;
};
export const selectDeviceReleaseInfo = (state: DeviceRootState) => {
const device = selectDevice(state);
return device?.firmwareRelease ?? null;
};
export const selectDeviceFirmwareVersion = memoize((state: DeviceRootState) => {
const device = selectDevice(state);
return getFirmwareVersionArray(device);
});

View File

@@ -32,7 +32,7 @@ export const DeviceControlButtons = () => {
if (!selectedDevice) return null;
const handleEject = () => {
dispatch(deviceActions.forgetDevice(selectedDevice));
dispatch(deviceActions.deviceDisconnect(selectedDevice));
};
const handleDeviceRedirect = () => {

View File

@@ -3,7 +3,11 @@ import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { Button, Text, VStack } from '@suite-native/atoms';
import { selectDevices, selectIsSelectedDeviceImported } from '@suite-common/wallet-core';
import {
selectDevices,
selectIsSelectedDeviceImported,
selectSelectedDeviceId,
} from '@suite-common/wallet-core';
import {
ConnectDeviceStackRoutes,
RootStackParamList,
@@ -29,6 +33,7 @@ export const DeviceManagerContent = () => {
const { translate } = useTranslate();
const devices = useSelector(selectDevices);
const selectedDeviceId = useSelector(selectSelectedDeviceId);
const isPortfolioTrackerDevice = useSelector(selectIsSelectedDeviceImported);
const { setIsDeviceManagerVisible } = useDeviceManager();
@@ -47,9 +52,12 @@ export const DeviceManagerContent = () => {
<Text variant="callout">
<Translation id="deviceManager.deviceList.sectionTitle" />
</Text>
{devices.map(device => (
<DeviceItem key={device.path} id={device.id} />
))}
{devices.map(device => {
if (device.id !== selectedDeviceId) {
return <DeviceItem key={device.path} id={device.id} />;
}
return null;
})}
</VStack>
{isPortfolioTrackerDevice && (
<VStack>

View File

@@ -21,11 +21,15 @@
"@suite-native/feature-flags": "workspace:*",
"@suite-native/helpers": "workspace:*",
"@suite-native/intl": "workspace:*",
"@suite-native/link": "workspace:*",
"@suite-native/module-settings": "workspace:*",
"@suite-native/navigation": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/device-utils": "workspace:*",
"@trezor/styles": "workspace:*",
"react": "18.2.0",
"react-redux": "8.0.7"
"react-native": "0.71.8",
"react-redux": "8.0.7",
"semver": "^7.5.4"
}
}

View File

@@ -0,0 +1,53 @@
import { BottomSheet, VStack, Box, Button, Text } from '@suite-native/atoms';
import { Translation, useTranslate } from '@suite-native/intl';
import { useOpenLink } from '@suite-native/link';
export type HowToUpdateBottomSheetProps = {
isVisible: boolean;
onClose: (isVisible: boolean) => void;
title?: string;
};
export const HowToUpdateBottomSheet = ({
isVisible,
onClose,
title,
}: HowToUpdateBottomSheetProps) => {
const { translate } = useTranslate();
const openLink = useOpenLink();
const handleHelpClick = () => {
openLink('https://trezor.io/learn/a/update-trezor-device-firmware');
onClose(false);
};
return (
<BottomSheet isVisible={isVisible} onClose={onClose} title={title}>
<VStack spacing="large">
<VStack paddingHorizontal="large">
<Text variant="callout">
<Translation id="deviceInfo.updateHowTo.subtitle" />
</Text>
<Box>
<Text color="textSubdued">
<Translation id="deviceInfo.updateHowTo.lines.1" />
</Text>
<Text color="textSubdued">
<Translation id="deviceInfo.updateHowTo.lines.2" />
</Text>
<Text color="textSubdued">
<Translation id="deviceInfo.updateHowTo.lines.3" />
</Text>
</Box>
</VStack>
<Button
colorScheme="tertiaryElevation0"
onPress={handleHelpClick}
iconRight="arrowUpRight"
>
{translate('deviceInfo.updateHowTo.button')}
</Button>
</VStack>
</BottomSheet>
);
};

View File

@@ -18,10 +18,10 @@ import {
selectIsDeviceConnectedAndAuthorized,
selectIsSelectedDeviceImported,
selectIsUnacquiredDevice,
selectDeviceFirmwareVersion,
selectDeviceModel,
selectDevice,
deviceActions,
selectDeviceFirmwareVersion,
} from '@suite-common/wallet-core';
import { useAlert } from '@suite-native/alerts';
import { Translation, useTranslate } from '@suite-native/intl';
@@ -56,15 +56,15 @@ type NavigationProps = StackToStackCompositeNavigationProps<
>;
export const useDeviceConnect = () => {
const deviceModel = useSelector(selectDeviceModel);
const currentDevice = useSelector(selectDevice);
const hasDeviceRequestedPin = useSelector(selectDeviceRequestedPin);
const isDeviceConnectedAndAuthorized = useSelector(selectIsDeviceConnectedAndAuthorized);
const isUnacquiredDevice = useSelector(selectIsUnacquiredDevice);
const isConnectedDeviceUninitialized = useSelector(selectIsConnectedDeviceUninitialized);
const isSelectedDeviceImported = useSelector(selectIsSelectedDeviceImported);
const firmwareVersion = useSelector(selectDeviceFirmwareVersion);
const deviceModel = useSelector(selectDeviceModel);
const currentDevice = useSelector(selectDevice);
const isOnboardingFinished = useSelector(selectIsOnboardingFinished);
const deviceFwVersion = useSelector(selectDeviceFirmwareVersion);
const dispatch = useDispatch();
@@ -80,6 +80,8 @@ export const useDeviceConnect = () => {
}
}, [currentDevice, dispatch]);
const isFirmwareSupported = isFirmwareVersionSupported(deviceFwVersion, deviceModel);
useEffect(() => {
if (hasDeviceRequestedPin) {
navigation.navigate(RootStackRoutes.ConnectDevice, {
@@ -106,7 +108,7 @@ export const useDeviceConnect = () => {
}, [dispatch, hideAlert, isOnboardingFinished, isUnacquiredDevice, showAlert, translate]);
useEffect(() => {
if (isOnboardingFinished && !isFirmwareVersionSupported(firmwareVersion, deviceModel)) {
if (isOnboardingFinished && !isFirmwareSupported && !isSelectedDeviceImported) {
showAlert({
title: translate('moduleDevice.unsupportedFirmware.title'),
description: translate('moduleDevice.unsupportedFirmware.description'),
@@ -120,12 +122,12 @@ export const useDeviceConnect = () => {
}
}, [
dispatch,
firmwareVersion,
deviceModel,
isFirmwareSupported,
showAlert,
translate,
handleDisconnect,
isOnboardingFinished,
isSelectedDeviceImported,
]);
useEffect(() => {

View File

@@ -53,7 +53,10 @@ export const prepareDeviceMiddleware = createMiddlewareWithExtraDeps(
dispatch(authorizeDevice({ isUseEmptyPassphraseForced: true }));
}
if (deviceActions.forgetDevice.match(action)) {
if (
deviceActions.forgetDevice.match(action) ||
deviceActions.forgetAndDisconnectDevice.match(action)
) {
dispatch(handleDeviceDisconnect(action.payload));
}

View File

@@ -1,6 +1,7 @@
import { useSelector } from 'react-redux';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { G } from '@mobily/ts-belt';
import { CommonActions, useNavigation } from '@react-navigation/native';
import { DeviceModelInternal } from '@trezor/connect';
@@ -9,19 +10,26 @@ import {
Box,
Card,
HStack,
VStack,
Button,
IconButton,
ScreenHeaderWrapper,
Text,
} from '@suite-native/atoms';
import { HomeStackRoutes, RootStackRoutes, Screen } from '@suite-native/navigation';
import {
selectDeviceFirmwareVersion,
selectDevice,
selectDeviceModel,
selectDeviceReleaseInfo,
selectIsSelectedDeviceImported,
selectSelectedDeviceName,
} from '@suite-common/wallet-core';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { useTranslate } from '@suite-native/intl';
import { getFirmwareVersion } from '@trezor/device-utils';
import { useOpenLink } from '@suite-native/link';
import { HowToUpdateBottomSheet } from '../components/HowToUpdateBottomSheet';
const deviceImage = {
[DeviceModelInternal.T1B1]: require('../assets/t1.png'),
@@ -33,18 +41,44 @@ const emptyBoxStyle = prepareNativeStyle(() => ({
width: 48,
}));
const contentStyle = prepareNativeStyle(() => ({
flexGrow: 1,
}));
export const DeviceInfoModalScreen = () => {
const navigation = useNavigation();
const { translate } = useTranslate();
const openLink = useOpenLink();
const deviceModel = useSelector(selectDeviceModel);
const deviceName = useSelector(selectSelectedDeviceName);
const deviceFirmwareVersion = useSelector(selectDeviceFirmwareVersion);
const device = useSelector(selectDevice);
const isPortfolioTrackerDevice = useSelector(selectIsSelectedDeviceImported);
const deviceReleaseInfo = useSelector(selectDeviceReleaseInfo);
const { applyStyle } = useNativeStyles();
const [isUpdateSheetOpen, setIsUpdateSheetOpen] = useState<boolean>(false);
const isUpgradable = deviceReleaseInfo?.isNewer ?? false;
const getCardAlertProps = () => {
if (G.isNotNullable(deviceReleaseInfo)) {
if (isUpgradable) {
return {
alertTitle: translate('deviceInfo.outdatedFw'),
alertVariant: 'warning',
} as const;
}
return {
alertTitle: translate('deviceInfo.upToDateFw'),
alertVariant: 'success',
} as const;
}
return { alertTitle: undefined, alertVariant: undefined } as const;
};
const cardAlertProps = getCardAlertProps();
useEffect(() => {
if (isPortfolioTrackerDevice) {
// Should be part of useDeviceConnect hook
@@ -67,12 +101,17 @@ export const DeviceInfoModalScreen = () => {
if (!deviceModel) return null;
const currentFwVersion = getFirmwareVersion(device);
const handleGoBack = () => {
navigation.goBack();
};
const stringifiedFirmwareVersion = deviceFirmwareVersion?.join('.');
const handleAccessoriesClick = () => {
openLink('https://trezor.io/accessories');
};
const handleUpdateClick = () => setIsUpdateSheetOpen(true);
return (
<Screen
screenHeader={
@@ -88,17 +127,40 @@ export const DeviceInfoModalScreen = () => {
</ScreenHeaderWrapper>
}
>
<Card>
<HStack spacing="large">
<Image width={92} height={151} source={deviceImage[deviceModel]} />
<Box justifyContent="center">
<Text variant="titleSmall">{deviceName}</Text>
<Text variant="hint">
{translate('deviceInfo.installedFw', { stringifiedFirmwareVersion })}
</Text>
</Box>
</HStack>
</Card>
<Box style={applyStyle(contentStyle)}>
<Card {...cardAlertProps}>
<HStack spacing="large">
<Image width={92} height={151} source={deviceImage[deviceModel]} />
<Box justifyContent="center">
<Text variant="titleSmall">{deviceName}</Text>
<Text variant="hint">
{translate('deviceInfo.installedFw', {
version: currentFwVersion,
})}
</Text>
</Box>
</HStack>
</Card>
</Box>
<VStack spacing="medium">
<Button
colorScheme="tertiaryElevation0"
onPress={handleAccessoriesClick}
iconRight="arrowUpRight"
>
{translate('deviceInfo.goToAccessories')}
</Button>
{isUpgradable && (
<Button colorScheme="primary" onPress={handleUpdateClick}>
{translate('deviceInfo.updateHowTo.title')}
</Button>
)}
</VStack>
<HowToUpdateBottomSheet
isVisible={isUpdateSheetOpen}
onClose={setIsUpdateSheetOpen}
title={translate('deviceInfo.updateHowTo.title')}
/>
</Screen>
);
};

View File

@@ -1,4 +1,5 @@
import { A, G } from '@mobily/ts-belt';
import { G } from '@mobily/ts-belt';
import * as semver from 'semver';
import { DeviceModelInternal, VersionArray } from '@trezor/connect';
@@ -18,8 +19,8 @@ export const isFirmwareVersionSupported = (
if (!minimalVersion) return true;
return A.every(
version.map((v, i) => v >= minimalVersion[i]),
val => val,
);
const versionString = version.join('.');
const minimalVersionString = minimalVersion.join('.');
return semver.satisfies(versionString, `>=${minimalVersionString}`);
};

View File

@@ -13,9 +13,11 @@
{ "path": "../feature-flags" },
{ "path": "../helpers" },
{ "path": "../intl" },
{ "path": "../link" },
{ "path": "../module-settings" },
{ "path": "../navigation" },
{ "path": "../../packages/connect" },
{ "path": "../../packages/device-utils" },
{ "path": "../../packages/styles" }
]
}

View File

@@ -22,6 +22,7 @@
"@suite-native/accounts": "workspace:*",
"@suite-native/config": "workspace:*",
"@suite-native/device": "workspace:*",
"@trezor/connect": "workspace:*"
"@trezor/connect": "workspace:*",
"@trezor/device-utils": "workspace:*"
}
}

View File

@@ -2,8 +2,8 @@ import {
deviceActions,
selectDevice,
discoveryActions,
selectDeviceFirmwareVersion,
selectDeviceModel,
selectDeviceFirmwareVersion,
} from '@suite-common/wallet-core';
import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils';
import { isFirmwareVersionSupported } from '@suite-native/device';
@@ -19,12 +19,12 @@ export const prepareDiscoveryMiddleware = createMiddlewareWithExtraDeps(
const device = selectDevice(getState());
const deviceModel = selectDeviceModel(getState());
const deviceFirmwareVersion = selectDeviceFirmwareVersion(getState());
const deviceFwVersion = selectDeviceFirmwareVersion(getState());
const areTestnetsEnabled = selectAreTestnetsEnabled(getState());
const isDeviceFirmwareVersionSupported = isFirmwareVersionSupported(
deviceFirmwareVersion,
deviceFwVersion,
deviceModel,
);

View File

@@ -26,6 +26,7 @@
{ "path": "../accounts" },
{ "path": "../config" },
{ "path": "../device" },
{ "path": "../../packages/connect" }
{ "path": "../../packages/connect" },
{ "path": "../../packages/device-utils" }
]
}

View File

@@ -305,6 +305,19 @@ export const en = {
},
deviceInfo: {
installedFw: 'Installed firmware: {version}',
upToDateFw: 'The firmware is up to date.',
outdatedFw: 'The firmware is outdated.',
goToAccessories: 'Get accessories @ Trezor Shop',
updateHowTo: {
title: 'How to update firmware',
subtitle: 'Follow these steps:',
lines: {
1: '1. Connect Trezor to Desktop Suite',
2: '2. Navigate to Settings menu',
3: '3. Install new firmware',
},
button: 'Learn more @ Trezor.io',
},
},
qrCode: {
addressCopied: 'Address copied',

View File

@@ -7385,12 +7385,16 @@ __metadata:
"@suite-native/feature-flags": "workspace:*"
"@suite-native/helpers": "workspace:*"
"@suite-native/intl": "workspace:*"
"@suite-native/link": "workspace:*"
"@suite-native/module-settings": "workspace:*"
"@suite-native/navigation": "workspace:*"
"@trezor/connect": "workspace:*"
"@trezor/device-utils": "workspace:*"
"@trezor/styles": "workspace:*"
react: "npm:18.2.0"
react-native: "npm:0.71.8"
react-redux: "npm:8.0.7"
semver: "npm:^7.5.4"
languageName: unknown
linkType: soft
@@ -7411,6 +7415,7 @@ __metadata:
"@suite-native/config": "workspace:*"
"@suite-native/device": "workspace:*"
"@trezor/connect": "workspace:*"
"@trezor/device-utils": "workspace:*"
languageName: unknown
linkType: soft