diff --git a/packages/device-utils/src/firmwareUtils.ts b/packages/device-utils/src/firmwareUtils.ts
index bf38d80c73..6d43810a0c 100644
--- a/packages/device-utils/src/firmwareUtils.ts
+++ b/packages/device-utils/src/firmwareUtils.ts
@@ -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}`;
};
diff --git a/suite-common/wallet-core/src/device/deviceActions.ts b/suite-common/wallet-core/src/device/deviceActions.ts
index 4436b19548..727046bde3 100644
--- a/suite-common/wallet-core/src/device/deviceActions.ts
+++ b/suite-common/wallet-core/src/device/deviceActions.ts
@@ -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,
};
diff --git a/suite-common/wallet-core/src/device/deviceReducer.ts b/suite-common/wallet-core/src/device/deviceReducer.ts
index d789465fe3..a813f6f241 100644
--- a/suite-common/wallet-core/src/device/deviceReducer.ts
+++ b/suite-common/wallet-core/src/device/deviceReducer.ts
@@ -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);
+});
diff --git a/suite-native/device-manager/src/components/DeviceControlButtons.tsx b/suite-native/device-manager/src/components/DeviceControlButtons.tsx
index 6f8fc62fbd..6f0ba8b537 100644
--- a/suite-native/device-manager/src/components/DeviceControlButtons.tsx
+++ b/suite-native/device-manager/src/components/DeviceControlButtons.tsx
@@ -32,7 +32,7 @@ export const DeviceControlButtons = () => {
if (!selectedDevice) return null;
const handleEject = () => {
- dispatch(deviceActions.forgetDevice(selectedDevice));
+ dispatch(deviceActions.deviceDisconnect(selectedDevice));
};
const handleDeviceRedirect = () => {
diff --git a/suite-native/device-manager/src/components/DeviceManagerContent.tsx b/suite-native/device-manager/src/components/DeviceManagerContent.tsx
index 2d2c4b4d56..6b0b763b5b 100644
--- a/suite-native/device-manager/src/components/DeviceManagerContent.tsx
+++ b/suite-native/device-manager/src/components/DeviceManagerContent.tsx
@@ -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 = () => {
- {devices.map(device => (
-
- ))}
+ {devices.map(device => {
+ if (device.id !== selectedDeviceId) {
+ return ;
+ }
+ return null;
+ })}
{isPortfolioTrackerDevice && (
diff --git a/suite-native/device/package.json b/suite-native/device/package.json
index 2514c54c1b..5e999c69ed 100644
--- a/suite-native/device/package.json
+++ b/suite-native/device/package.json
@@ -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"
}
}
diff --git a/suite-native/device/src/components/HowToUpdateBottomSheet.tsx b/suite-native/device/src/components/HowToUpdateBottomSheet.tsx
new file mode 100644
index 0000000000..51595c7fd5
--- /dev/null
+++ b/suite-native/device/src/components/HowToUpdateBottomSheet.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/suite-native/device/src/hooks/useDeviceConnect.tsx b/suite-native/device/src/hooks/useDeviceConnect.tsx
index 56fbfc8fc8..15698f4b73 100644
--- a/suite-native/device/src/hooks/useDeviceConnect.tsx
+++ b/suite-native/device/src/hooks/useDeviceConnect.tsx
@@ -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(() => {
diff --git a/suite-native/device/src/middlewares/deviceMiddleware.ts b/suite-native/device/src/middlewares/deviceMiddleware.ts
index 6cac1226ba..61ada9c918 100644
--- a/suite-native/device/src/middlewares/deviceMiddleware.ts
+++ b/suite-native/device/src/middlewares/deviceMiddleware.ts
@@ -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));
}
diff --git a/suite-native/device/src/screens/DeviceInfoModalScreen.tsx b/suite-native/device/src/screens/DeviceInfoModalScreen.tsx
index 7180114b50..42994f8fc6 100644
--- a/suite-native/device/src/screens/DeviceInfoModalScreen.tsx
+++ b/suite-native/device/src/screens/DeviceInfoModalScreen.tsx
@@ -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(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 (
{
}
>
-
-
-
-
- {deviceName}
-
- {translate('deviceInfo.installedFw', { stringifiedFirmwareVersion })}
-
-
-
-
+
+
+
+
+
+ {deviceName}
+
+ {translate('deviceInfo.installedFw', {
+ version: currentFwVersion,
+ })}
+
+
+
+
+
+
+
+ {isUpgradable && (
+
+ )}
+
+
);
};
diff --git a/suite-native/device/src/utils.ts b/suite-native/device/src/utils.ts
index d8a12a46bb..7e98f7d251 100644
--- a/suite-native/device/src/utils.ts
+++ b/suite-native/device/src/utils.ts
@@ -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}`);
};
diff --git a/suite-native/device/tsconfig.json b/suite-native/device/tsconfig.json
index fd5b6895ec..a8c6fc60fd 100644
--- a/suite-native/device/tsconfig.json
+++ b/suite-native/device/tsconfig.json
@@ -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" }
]
}
diff --git a/suite-native/discovery/package.json b/suite-native/discovery/package.json
index 1a9ca83261..4e6cbc7328 100644
--- a/suite-native/discovery/package.json
+++ b/suite-native/discovery/package.json
@@ -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:*"
}
}
diff --git a/suite-native/discovery/src/discoveryMiddleware.ts b/suite-native/discovery/src/discoveryMiddleware.ts
index 167ec301b6..17491883f7 100644
--- a/suite-native/discovery/src/discoveryMiddleware.ts
+++ b/suite-native/discovery/src/discoveryMiddleware.ts
@@ -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,
);
diff --git a/suite-native/discovery/tsconfig.json b/suite-native/discovery/tsconfig.json
index 1d1fe6e4eb..adcb4d1f67 100644
--- a/suite-native/discovery/tsconfig.json
+++ b/suite-native/discovery/tsconfig.json
@@ -26,6 +26,7 @@
{ "path": "../accounts" },
{ "path": "../config" },
{ "path": "../device" },
- { "path": "../../packages/connect" }
+ { "path": "../../packages/connect" },
+ { "path": "../../packages/device-utils" }
]
}
diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index d1c0ce0ba4..08c3708794 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -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',
diff --git a/yarn.lock b/yarn.lock
index c92d828db8..67b1e067ff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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