Skip to content

Commit

Permalink
feat(mobile): firmware upgrade (#15184)
Browse files Browse the repository at this point in the history
* fix(connect): make fetch work in react native

* refactor(suite): move FW upgrade logic to common package

* feat(mobile): make some atoms easily animatable

* feat(mobile): firmware upgrade

* fix: formatting and deps

* refactor(suite): map operation to translation ID

* refactor(suite): add transport selectors

* refactor(suite): remove useFirmware hook

* chore: bug fixes

---------

Co-authored-by: Jan Komarek <jan.komarek@satoshilabs.com>
  • Loading branch information
Nodonisko and komret authored Dec 3, 2024
1 parent 0eee069 commit b870aa9
Show file tree
Hide file tree
Showing 98 changed files with 1,630 additions and 299 deletions.
10 changes: 4 additions & 6 deletions packages/connect/src/data/downloadReleasesMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ export const downloadReleasesMetadata = async ({
}: DownloadReleasesMetadataParams): Promise<FirmwareRelease[]> => {
const url = `https://data.trezor.io/firmware/${internal_model.toLowerCase()}/releases.json`;

const response = await httpRequest(
url,
'json',
{ signal: AbortSignal.timeout(10000) },
true, // skipLocalForceDownload=true
);
const response = await httpRequest(url, 'json', {
signal: AbortSignal.timeout(10000),
skipLocalForceDownload: true,
});

if (isValidReleases(response)) {
return response;
Expand Down
18 changes: 9 additions & 9 deletions packages/connect/src/utils/assets-browser.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
// origin https://github.com/trezor/connect/blob/develop/src/js/env/browser/networkUtils.js

import fetch from 'cross-fetch';

export const httpRequest = async (
import { HttpRequestType, HttpRequestReturnType, HttpRequestOptions } from './assetsTypes';

export const httpRequest = async <T extends HttpRequestType>(
url: string,
type: 'text' | 'binary' | 'json' = 'text',
options?: RequestInit,
) => {
type: T = 'text' as T,
options?: HttpRequestOptions,
): Promise<HttpRequestReturnType<T>> => {
const init: RequestInit = { ...options, credentials: 'same-origin' };

const response = await fetch(url, init);
if (response.ok) {
if (type === 'json') {
const txt = await response.text();

return JSON.parse(txt);
return JSON.parse(txt) as HttpRequestReturnType<T>;
}
if (type === 'binary') {
return response.arrayBuffer();
return response.arrayBuffer() as Promise<HttpRequestReturnType<T>>;
}

return response.text();
return response.text() as Promise<HttpRequestReturnType<T>>;
}

throw new Error(`httpRequest error: ${url} ${response.statusText}`);
Expand Down
31 changes: 30 additions & 1 deletion packages/connect/src/utils/assets.native.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
import { HttpRequestType, HttpRequestReturnType, HttpRequestOptions } from './assetsTypes';
import { tryLocalAssetRequire } from './assetUtils';

export const httpRequest = (url: string, _type: string): any => tryLocalAssetRequire(url);
export function httpRequest<T extends HttpRequestType>(
url: string,
type: T,
options?: HttpRequestOptions,
): Promise<HttpRequestReturnType<T>> {
const asset = options?.skipLocalForceDownload ? null : tryLocalAssetRequire(url);

if (!asset) {
return fetch(url, {
...options,
})
.then(response => {
if (type === 'binary') {
return response.arrayBuffer() as unknown as HttpRequestReturnType<T>;
}
if (type === 'json') {
return response.json() as unknown as HttpRequestReturnType<T>;
}

return response.text() as unknown as HttpRequestReturnType<T>;
})
.catch(error => {
console.error('httpRequest native error', error);
throw error;
});
}

return asset as Promise<HttpRequestReturnType<T>>;
}
41 changes: 10 additions & 31 deletions packages/connect/src/utils/assets.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,26 @@
// https://github.com/trezor/connect/blob/develop/src/js/env/node/networkUtils.js

import fetch from 'cross-fetch';
import { promises as fs } from 'fs';

import { httpRequest as browserHttpRequest } from './assets-browser';
import { HttpRequestOptions, HttpRequestReturnType, HttpRequestType } from './assetsTypes';
import { tryLocalAssetRequire } from './assetUtils';

if (global && typeof global.fetch !== 'function') {
global.fetch = fetch;
}

export function httpRequest(
url: string,
type: 'text',
options?: RequestInit,
skipLocalForceDownload?: boolean,
): Promise<string>;

export function httpRequest(
export function httpRequest<T extends HttpRequestType>(
url: string,
type: 'binary',
options?: RequestInit,
skipLocalForceDownload?: boolean,
): Promise<ArrayBuffer>;

export function httpRequest(
url: string,
type: 'json',
options?: RequestInit,
skipLocalForceDownload?: boolean,
): Promise<Record<string, any>>;

export function httpRequest(
url: any,
type: any,
options?: RequestInit,
skipLocalForceDownload?: boolean,
) {
const asset = skipLocalForceDownload ? null : tryLocalAssetRequire(url);
type: T,
options?: HttpRequestOptions,
): Promise<HttpRequestReturnType<T>> {
const asset = options?.skipLocalForceDownload ? null : tryLocalAssetRequire(url);

if (!asset) {
return /^https?/.test(url) ? browserHttpRequest(url, type, options) : fs.readFile(url);
return /^https?/.test(url)
? browserHttpRequest(url, type, options)
: (fs.readFile(url) as Promise<HttpRequestReturnType<T>>);
}

return asset;
return asset as Promise<HttpRequestReturnType<T>>;
}
13 changes: 13 additions & 0 deletions packages/connect/src/utils/assetsTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type HttpRequestType = 'text' | 'binary' | 'json';

export type HttpRequestReturnType<T extends HttpRequestType> = T extends 'text'
? string
: T extends 'binary'
? ArrayBuffer | Buffer
: T extends 'json'
? Record<string, any>
: never;

export interface HttpRequestOptions extends RequestInit {
skipLocalForceDownload?: boolean;
}
1 change: 1 addition & 0 deletions packages/eslint/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const eslint = [
'**/node_modules/*',
'**/public/*',
'**/ci/',
'**/.expo/*',
'eslint-local-rules/*',
],
},
Expand Down
1 change: 1 addition & 0 deletions packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@suite-common/connect-init": "workspace:*",
"@suite-common/device-authenticity": "workspace:*",
"@suite-common/fiat-services": "workspace:*",
"@suite-common/firmware": "workspace:*",
"@suite-common/formatters": "workspace:*",
"@suite-common/icons": "workspace:*",
"@suite-common/intl-types": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { testMocks } from '@suite-common/test-utils';
import { firmwareUpdate, firmwareActions } from '@suite-common/wallet-core';
import { firmwareUpdate, firmwareActions } from '@suite-common/firmware';
import { UI, DeviceModelInternal, FirmwareType } from '@trezor/connect';

const { getSuiteDevice, getFirmwareRelease } = testMocks;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { testMocks } from '@suite-common/test-utils';
import { prepareFirmwareReducer, State as DeviceState } from '@suite-common/wallet-core';
import { State as DeviceState } from '@suite-common/wallet-core';
import { DeviceModelInternal } from '@trezor/connect';
import { prepareFirmwareReducer } from '@suite-common/firmware';

import { configureStore, filterThunkActionTypes } from 'src/support/tests/configureStore';
import suiteReducer from 'src/reducers/suite/suiteReducer';
Expand Down
8 changes: 2 additions & 6 deletions packages/suite/src/actions/settings/deviceSettingsActions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {
selectDevices,
selectDevice,
deviceActions,
FIRMWARE_MODULE_PREFIX,
} from '@suite-common/wallet-core';
import { selectDevices, selectDevice, deviceActions } from '@suite-common/wallet-core';
import { FIRMWARE_MODULE_PREFIX } from '@suite-common/firmware';
import * as deviceUtils from '@suite-common/suite-utils';
import TrezorConnect, { ERRORS } from '@trezor/connect';
import { analytics, EventType } from '@trezor/suite-analytics';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
selectDevice,
selectDevices,
selectDevicesCount,
prepareFirmwareReducer,
deviceActions,
acquireDevice,
authConfirm,
Expand All @@ -20,6 +19,7 @@ import {
createDeviceInstanceThunk,
ConnectDeviceSettings,
} from '@suite-common/wallet-core';
import { prepareFirmwareReducer } from '@suite-common/firmware';
import { connectInitThunk } from '@suite-common/connect-init';
import { DEVICE } from '@trezor/connect';

Expand Down
10 changes: 6 additions & 4 deletions packages/suite/src/components/firmware/FirmwareInitial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { getFirmwareVersion, isBitcoinOnlyDevice } from '@trezor/device-utils';
import { FirmwareType } from '@trezor/connect';
import { selectDevices } from '@suite-common/wallet-core';
import { spacingsPx } from '@trezor/theme';
import { useFirmwareInstallation } from '@suite-common/firmware';

import { OnboardingStepBox, OnboardingButtonSkip } from 'src/components/onboarding';
import { useDevice, useFirmware, useOnboarding, useSelector } from 'src/hooks/suite';
import { useDevice, useOnboarding, useSelector } from 'src/hooks/suite';
import { FirmwareInstallButton, FirmwareOffer } from 'src/components/firmware';

import { PrerequisitesGuide, Translation } from '../suite';
Expand Down Expand Up @@ -126,9 +127,10 @@ export const FirmwareInitial = ({
}: FirmwareInitialProps) => {
const [bitcoinOnlyOffer, setBitcoinOnlyOffer] = useState(false);
const { device } = useDevice();
const { deviceWillBeWiped, firmwareUpdate, setStatus, targetFirmwareType } = useFirmware({
shouldSwitchFirmwareType,
});
const { deviceWillBeWiped, firmwareUpdate, setStatus, targetFirmwareType } =
useFirmwareInstallation({
shouldSwitchFirmwareType,
});
const { goToNextStep, updateAnalytics } = useOnboarding();
const devices = useSelector(selectDevices);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import styled from 'styled-components';
import { Button } from '@trezor/components';
import { UI } from '@trezor/connect';
import { spacingsPx } from '@trezor/theme';
import { useFirmwareInstallation } from '@suite-common/firmware';

import { Translation, WebUsbButton } from 'src/components/suite';
import { useFirmware } from 'src/hooks/suite';
import { FirmwareOffer, FirmwareProgressBar, ReconnectDevicePrompt } from 'src/components/firmware';
import { OnboardingStepBox } from 'src/components/onboarding';
import { TrezorDevice } from 'src/types/suite';
import { selectIsActionAbortable } from 'src/reducers/suite/suiteReducer';
import { selectIsActionAbortable, selectIsWebUsb } from 'src/reducers/suite/suiteReducer';
import { useSelector } from 'src/hooks/suite/useSelector';

const SelectDevice = styled.div`
Expand Down Expand Up @@ -40,12 +40,13 @@ export const FirmwareInstallation = ({
onPromptClose,
onSuccess,
}: FirmwareInstallationProps) => {
const { status, isWebUSB, showReconnectPrompt, uiEvent, targetType } = useFirmware();
const { status, showReconnectPrompt, uiEvent, targetType } = useFirmwareInstallation();
const isActionAbortable = useSelector(selectIsActionAbortable);
const isWebUsbTransport = useSelector(selectIsWebUsb);

const getInnerActionComponent = () => {
if (
isWebUSB &&
isWebUsbTransport &&
uiEvent?.type === UI.FIRMWARE_RECONNECT &&
uiEvent.payload.disconnected &&
uiEvent.payload.i > 2 && // Add some latency for cases when the device is already paired or is restarting.
Expand Down
5 changes: 3 additions & 2 deletions packages/suite/src/components/firmware/FirmwareOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { Icon, Markdown, Tooltip, variables } from '@trezor/components';
import { getFirmwareVersion } from '@trezor/device-utils';
import { FirmwareType } from '@trezor/connect';
import { spacingsPx } from '@trezor/theme';
import { useFirmwareInstallation } from '@suite-common/firmware';

import { Translation, TrezorLink } from 'src/components/suite';
import { useFirmware, useTranslation, useSelector } from 'src/hooks/suite';
import { useTranslation, useSelector } from 'src/hooks/suite';
import { getSuiteFirmwareTypeString } from 'src/utils/firmware';

const FwVersionWrapper = styled.div`
Expand Down Expand Up @@ -59,7 +60,7 @@ interface FirmwareOfferProps {

export const FirmwareOffer = ({ customFirmware, targetFirmwareType }: FirmwareOfferProps) => {
const useDevkit = useSelector(state => state.firmware.useDevkit);
const { originalDevice } = useFirmware();
const { originalDevice } = useFirmwareInstallation();
const { translationString } = useTranslation();

if (!originalDevice?.firmwareRelease) {
Expand Down
22 changes: 19 additions & 3 deletions packages/suite/src/components/firmware/FirmwareProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import styled, { useTheme } from 'styled-components';

import { Icon, ProgressBar, variables } from '@trezor/components';
import { borders, spacingsPx } from '@trezor/theme';
import { TranslationKey } from '@suite-common/intl-types';
import { FirmwareOperationStatus, useFirmwareInstallation } from '@suite-common/firmware';

import { useFirmware } from 'src/hooks/suite';
import { Translation } from '../suite';

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -49,15 +51,29 @@ const Percentage = styled.div`
height: ${spacingsPx.xl};
`;

const mapOperationToTransaltionId: Record<
NonNullable<FirmwareOperationStatus['operation']>,
TranslationKey
> = {
installing: 'TR_INSTALLING',
validating: 'TR_VALIDATION',
restarting: 'TR_WAIT_FOR_REBOOT',
completed: 'TR_FIRMWARE_STATUS_INSTALLATION_COMPLETED',
};

export const FirmwareProgressBar = () => {
const theme = useTheme();
const { operation, progress } = useFirmware();
const { operation, progress } = useFirmwareInstallation();

const isDone = progress === 100;

return (
<Wrapper>
<Label>{operation}</Label>
{operation && (
<Label>
<Translation id={mapOperationToTransaltionId[operation]} />
</Label>
)}
<StyledProgressBar value={progress} />
<Percentage>
{isDone ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { INVALID_HASH_ERROR } from '@suite-common/wallet-core';
import { INVALID_HASH_ERROR } from '@suite-common/firmware';

import { OnboardingStepBox } from '../onboarding';
import { Translation } from '../suite';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { ConfirmOnDevice } from '@trezor/product-components';
import { TranslationKey } from '@suite-common/intl-types';
import { spacings } from '@trezor/theme';
import { selectDeviceLabelOrName } from '@suite-common/wallet-core';
import { useFirmwareInstallation } from '@suite-common/firmware';

import { useDevice, useFirmware, useSelector } from 'src/hooks/suite';
import { useDevice, useSelector } from 'src/hooks/suite';
import { DeviceConfirmImage } from 'src/components/suite/DeviceConfirmImage';
import { Translation, WebUsbButton } from 'src/components/suite';
import { selectIsWebUsb } from 'src/reducers/suite/suiteReducer';

const RebootDeviceGraphics = ({
device,
Expand Down Expand Up @@ -62,7 +64,8 @@ interface ReconnectDevicePromptProps {

export const ReconnectDevicePrompt = ({ onClose, onSuccess }: ReconnectDevicePromptProps) => {
const deviceLabel = useSelector(selectDeviceLabelOrName);
const { showManualReconnectPrompt, isWebUSB, status, uiEvent } = useFirmware();
const isWebUsbTransport = useSelector(selectIsWebUsb);
const { showManualReconnectPrompt, status, uiEvent } = useFirmwareInstallation();
const { device } = useDevice();

const isManualRebootRequired =
Expand Down Expand Up @@ -93,7 +96,7 @@ export const ReconnectDevicePrompt = ({ onClose, onSuccess }: ReconnectDevicePro
const deviceModelInternal = device?.features?.internal_model;
const isAbortable =
onClose !== undefined && isManualRebootRequired && rebootPhase == 'waiting-for-reboot';
const showWebUsbButton = rebootPhase === 'disconnected' && isWebUSB;
const showWebUsbButton = rebootPhase === 'disconnected' && isWebUsbTransport;

const toNormal =
uiEvent?.type === UI.FIRMWARE_RECONNECT &&
Expand Down
Loading

0 comments on commit b870aa9

Please sign in to comment.