Skip to content

Commit 70d9e3b

Browse files
committed
feat(suite): update firmware components and modal
1 parent 024c979 commit 70d9e3b

File tree

15 files changed

+552
-446
lines changed

15 files changed

+552
-446
lines changed

packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ test.describe('Custom firmware', { tag: ['@group=device-management'] }, () => {
1313
test('Custom firmware installation', async ({ page }) => {
1414
await test.step('Start `Install firmware` flow', async () => {
1515
await page.getByTestId('@settings/device/custom-firmware-modal-button').click();
16-
await expect(page.getByTestId('@firmware-modal/install-button')).toBeDisabled();
16+
await expect(page.getByTestId('@firmware/install-button')).toBeDisabled();
1717
});
1818

1919
await test.step('Select the custom firmware', async () => {
2020
const fileChooserPromise = page.waitForEvent('filechooser');
21-
await page.getByTestId('@firmware-modal/input-area').click();
21+
await page.getByTestId('@firmware/input-area').click();
2222
const fileChooser = await fileChooserPromise;
2323
await fileChooser.setFiles(firmwarePath);
2424
});
2525

2626
await test.step('Complete the FW installation on the device', async () => {
27-
await page.getByTestId('@firmware-modal/install-button').click();
27+
await page.getByTestId('@firmware/install-button').click();
2828
await page.getByTestId('@firmware/confirm-seed-checkbox').click();
2929
await page.getByTestId('@firmware/confirm-seed-button').click();
3030
await expect(page.getByTestId('@firmware/reconnect-device')).toBeVisible();
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,53 @@
1-
import { useState } from 'react';
2-
3-
import styled from 'styled-components';
4-
51
import { selectIsDeviceBackedUp, selectSelectedDeviceLabelOrName } from '@suite-common/wallet-core';
6-
import { Button, Checkbox, Column, variables } from '@trezor/components';
7-
import { spacings, spacingsPx } from '@trezor/theme';
2+
import { Banner, Card, Checkbox, Column, H4, Paragraph } from '@trezor/components';
3+
import { spacings } from '@trezor/theme';
84

9-
import { OnboardingStepBox } from 'src/components/onboarding';
105
import { Translation } from 'src/components/suite';
11-
import { useDevice, useSelector } from 'src/hooks/suite';
12-
13-
import { FirmwareButtonsRow } from '../Buttons/FirmwareButtonsRow';
14-
import { FirmwareSwitchWarning } from '../FirmwareSwitchWarning';
15-
import { FirmwareInstallationBackupButton } from './FirmwareInstallationBackupButton';
16-
17-
const StyledSwitchWarning = styled(FirmwareSwitchWarning)`
18-
align-self: flex-start;
19-
border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY};
20-
color: ${({ theme }) => theme.legacy.TYPE_DARK_GREY};
21-
font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD};
22-
margin: ${spacingsPx.xs} ${spacingsPx.md};
23-
padding-bottom: ${spacingsPx.md};
24-
text-transform: uppercase;
25-
`;
6+
import { useSelector } from 'src/hooks/suite';
267

278
type CheckSeedStepProps = {
289
deviceWillBeWiped: boolean;
29-
onClose?: () => void;
30-
onSuccess: () => void;
10+
setIsChecked: (isChecked: boolean) => void;
11+
isChecked: boolean;
3112
};
3213

33-
export const CheckSeedStep = ({ deviceWillBeWiped, onClose, onSuccess }: CheckSeedStepProps) => {
14+
export const CheckSeedStep = ({
15+
deviceWillBeWiped,
16+
setIsChecked,
17+
isChecked,
18+
}: CheckSeedStepProps) => {
3419
const deviceLabel = useSelector(selectSelectedDeviceLabelOrName);
3520
const isDeviceBackedUp = useSelector(selectIsDeviceBackedUp);
36-
const { device } = useDevice();
37-
const [isChecked, setIsChecked] = useState(false);
3821

39-
const handleCheckboxClick = () => setIsChecked(prev => !prev);
4022
const getContent = () => {
41-
const noBackupHeading = (
42-
<Translation id="TR_DEVICE_LABEL_IS_NOT_BACKED_UP" values={{ deviceLabel }} />
43-
);
44-
4523
if (deviceWillBeWiped) {
4624
return {
4725
heading: isDeviceBackedUp ? (
4826
<Translation id="TR_CONTINUE_ONLY_WITH_SEED" />
4927
) : (
50-
noBackupHeading
28+
<Translation id="TR_DEVICE_LABEL_IS_NOT_BACKED_UP" values={{ deviceLabel }} />
5129
),
5230
description: (
53-
<Column gap={spacings.md}>
54-
<Translation
55-
id={
56-
isDeviceBackedUp
57-
? 'TR_CONTINUE_ONLY_WITH_SEED_DESCRIPTION'
58-
: 'TR_SWITCH_FIRMWARE_NO_BACKUP'
59-
}
60-
/>
61-
<Translation
62-
id={
63-
isDeviceBackedUp
64-
? 'TR_CONTINUE_ONLY_WITH_SEED_DESCRIPTION_2'
65-
: 'TR_SWITCH_FIRMWARE_NO_BACKUP_2'
66-
}
67-
/>
68-
</Column>
31+
<>
32+
<Paragraph variant="tertiary">
33+
<Translation
34+
id={
35+
isDeviceBackedUp
36+
? 'TR_CONTINUE_ONLY_WITH_SEED_DESCRIPTION'
37+
: 'TR_SWITCH_FIRMWARE_NO_BACKUP'
38+
}
39+
/>
40+
</Paragraph>
41+
<Paragraph variant="tertiary">
42+
<Translation
43+
id={
44+
isDeviceBackedUp
45+
? 'TR_CONTINUE_ONLY_WITH_SEED_DESCRIPTION_2'
46+
: 'TR_SWITCH_FIRMWARE_NO_BACKUP_2'
47+
}
48+
/>
49+
</Paragraph>
50+
</>
6951
),
7052
checkbox: <Translation id="TR_READ_AND_UNDERSTOOD" />,
7153
};
@@ -74,53 +56,48 @@ export const CheckSeedStep = ({ deviceWillBeWiped, onClose, onSuccess }: CheckSe
7456
return isDeviceBackedUp
7557
? {
7658
heading: <Translation id="TR_SECURITY_CHECKPOINT_GOT_SEED" />,
77-
description: <Translation id="TR_BEFORE_ANY_FURTHER_ACTIONS" />,
59+
description: (
60+
<Paragraph variant="tertiary">
61+
<Translation id="TR_BEFORE_ANY_FURTHER_ACTIONS" />
62+
</Paragraph>
63+
),
7864
checkbox: <Translation id="FIRMWARE_USER_HAS_SEED_CHECKBOX_DESC" />,
7965
}
8066
: {
81-
heading: noBackupHeading,
82-
description: <Translation id="TR_FIRMWARE_IS_POTENTIALLY_RISKY" />,
67+
heading: (
68+
<Translation id="TR_DEVICE_LABEL_IS_NOT_BACKED_UP" values={{ deviceLabel }} />
69+
),
70+
description: (
71+
<Paragraph variant="tertiary">
72+
<Translation id="TR_FIRMWARE_IS_POTENTIALLY_RISKY" />
73+
</Paragraph>
74+
),
8375
checkbox: <Translation id="FIRMWARE_USER_TAKES_RESPONSIBILITY_CHECKBOX_DESC" />,
8476
};
8577
};
8678

8779
const { heading, description, checkbox } = getContent();
8880

8981
return (
90-
<OnboardingStepBox
91-
image="FIRMWARE"
92-
heading={heading}
93-
description={description}
94-
innerActions={
95-
<FirmwareButtonsRow withCancelButton={deviceWillBeWiped} onClose={onClose}>
96-
<Button
97-
onClick={onSuccess}
98-
data-testid="@firmware/confirm-seed-button"
99-
isDisabled={!device?.connected || !isChecked}
100-
>
101-
<Translation
102-
id={deviceWillBeWiped ? 'TR_WIPE_AND_REINSTALL' : 'TR_CONTINUE'}
103-
/>
104-
</Button>
105-
<FirmwareInstallationBackupButton isBackedUp={isDeviceBackedUp} />
106-
</FirmwareButtonsRow>
107-
}
108-
disableConfirmWrapper
109-
nested
110-
>
82+
<Column gap={spacings.md}>
83+
<Column gap={spacings.xs} margin={{ bottom: spacings.xs }}>
84+
<H4>{heading}</H4>
85+
{description}
86+
</Column>
11187
{deviceWillBeWiped && (
112-
<StyledSwitchWarning>
88+
<Banner variant="destructive" icon="warning">
11389
<Translation id="TR_FIRMWARE_SWITCH_WARNING_3" />
114-
</StyledSwitchWarning>
90+
</Banner>
11591
)}
116-
<Checkbox
117-
isChecked={isChecked}
118-
onClick={handleCheckboxClick}
119-
margin={{ top: spacings.md }}
120-
data-testid="@firmware/confirm-seed-checkbox"
121-
>
122-
{checkbox}
123-
</Checkbox>
124-
</OnboardingStepBox>
92+
<Card>
93+
<Checkbox
94+
isChecked={isChecked}
95+
onClick={() => setIsChecked(!isChecked)}
96+
data-testid="@firmware/confirm-seed-checkbox"
97+
>
98+
{checkbox}
99+
</Checkbox>
100+
</Card>
101+
</Column>
125102
);
126103
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useFirmwareInstallation } from '@suite-common/firmware';
2+
import { ExtendedMessageDescriptor } from '@suite-common/intl-types';
3+
import { getFwUpdateVersion } from '@suite-common/suite-utils';
4+
import { Banner, Card, Column, Text } from '@trezor/components';
5+
import { FirmwareType } from '@trezor/connect';
6+
import { getFirmwareVersion } from '@trezor/device-utils';
7+
import { spacings } from '@trezor/theme';
8+
9+
import { FirmwareOffer } from 'src/components/firmware';
10+
import { useDevice } from 'src/hooks/suite';
11+
12+
import { Translation } from '../suite';
13+
14+
type GetDescriptionProps = {
15+
required: boolean;
16+
reinstall: boolean;
17+
targetType: FirmwareType;
18+
shouldSwitchFirmwareType?: boolean;
19+
isBitcoinOnlyAvailable?: boolean;
20+
};
21+
22+
const getDescription = ({
23+
required,
24+
reinstall,
25+
targetType,
26+
shouldSwitchFirmwareType,
27+
isBitcoinOnlyAvailable,
28+
}: GetDescriptionProps) => {
29+
if (shouldSwitchFirmwareType) {
30+
if (!isBitcoinOnlyAvailable) {
31+
return 'TR_BITCOIN_ONLY_UNAVAILABLE';
32+
}
33+
34+
return targetType === FirmwareType.BitcoinOnly
35+
? 'TR_SWITCH_TO_BITCOIN_ONLY_DESCRIPTION'
36+
: 'TR_SWITCH_TO_REGULAR_DESCRIPTION';
37+
}
38+
39+
if (required) {
40+
return 'TR_FIRMWARE_UPDATE_REQUIRED_EXPLAINED';
41+
}
42+
43+
return reinstall ? 'TR_FIRMWARE_REINSTALL_FW_DESCRIPTION' : 'TR_FIRMWARE_NEW_FW_DESCRIPTION';
44+
};
45+
46+
type FirmwareInitialProps = {
47+
shouldSwitchFirmwareType?: boolean;
48+
};
49+
50+
export const FirmwareInitialStandalone = ({
51+
shouldSwitchFirmwareType = false,
52+
}: FirmwareInitialProps) => {
53+
const { device } = useDevice();
54+
const { deviceWillBeWiped, targetFirmwareType } = useFirmwareInstallation({
55+
shouldSwitchFirmwareType,
56+
});
57+
58+
// Just to satisfy TS, disconnected device should be handled upstream.
59+
if (!device?.connected || !device?.features) {
60+
return null;
61+
}
62+
63+
// Bitcoin-only firmware is only available on T2T1 from v2.0.8 - older devices must first upgrade to 2.1.1 which does not have a Bitcoin-only variant
64+
const isBitcoinOnlyAvailable = !!device.firmwareRelease?.release.url_bitcoinonly;
65+
const currentFwVersion = getFirmwareVersion(device);
66+
const availableFwVersion = getFwUpdateVersion(device);
67+
const hasLatestAvailableFw = !!(
68+
availableFwVersion &&
69+
currentFwVersion &&
70+
availableFwVersion === currentFwVersion
71+
);
72+
73+
const warningTranslationValues: ExtendedMessageDescriptor['values'] = {
74+
b: chunks => <Text typographyStyle="callout">{chunks}</Text>,
75+
};
76+
77+
return (
78+
<Column gap={spacings.sm}>
79+
<Banner variant="info" icon="info">
80+
<Translation
81+
id={getDescription({
82+
/**
83+
* `device.firmware` is status of the firmware currently installed on the device.
84+
* available values: 'valid' | 'outdated' | 'required' | 'unknown' | 'none'
85+
*
86+
* `device.firmwareRelease` on the other hand contains latest available firmware to update to
87+
* (it is whatever returns getInfo() method from connect)
88+
* so it should not be used here.
89+
*/
90+
required: device.firmware === 'required',
91+
reinstall: device.firmware === 'valid' || hasLatestAvailableFw,
92+
targetType: targetFirmwareType,
93+
shouldSwitchFirmwareType,
94+
isBitcoinOnlyAvailable,
95+
})}
96+
values={{
97+
bitcoinOnly: <Translation id="TR_FIRMWARE_TYPE_BITCOIN_ONLY" />,
98+
regular: <Translation id="TR_FIRMWARE_TYPE_REGULAR" />,
99+
}}
100+
/>
101+
</Banner>
102+
{deviceWillBeWiped && (
103+
<>
104+
<Banner variant="destructive" icon="warning">
105+
<Translation
106+
id="TR_FIRMWARE_SWITCH_WARNING_1"
107+
values={warningTranslationValues}
108+
/>
109+
</Banner>
110+
<Banner variant="destructive" icon="warning">
111+
<Translation
112+
id="TR_FIRMWARE_SWITCH_WARNING_2"
113+
values={warningTranslationValues}
114+
/>
115+
</Banner>
116+
</>
117+
)}
118+
<Card>
119+
<FirmwareOffer targetFirmwareType={targetFirmwareType} />
120+
</Card>
121+
</Column>
122+
);
123+
};

packages/suite/src/components/firmware/FirmwareInstallation.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const FirmwareInstallation = ({
9090
nested={!!standaloneFwUpdate}
9191
disableConfirmWrapper={!!standaloneFwUpdate}
9292
>
93-
<FirmwareOffer customFirmware={customFirmware} targetFirmwareType={targetType} />
93+
<FirmwareOffer isCustomFirmware={customFirmware} targetFirmwareType={targetType} />
9494
<FirmwareProgressBar />
9595
</OnboardingStepBox>
9696
</>

0 commit comments

Comments
 (0)