mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-21 22:57:17 +01:00
feat(suite): refactor of the backup type
feat(suite): use FloatingUI to do the dropdown
This commit is contained in:
committed by
Peter Sanderson
parent
be86c395ad
commit
0549ee8557
@@ -5,11 +5,12 @@ import { Icon } from '@suite-common/icons/src/webComponents';
|
||||
import { borders, Color, CSSColor, spacings, spacingsPx, typography } from '@trezor/theme';
|
||||
import { focusStyleTransition, getFocusShadowStyle } from '../../utils/utils';
|
||||
import type { UISize, UIVariant } from '../../config/types';
|
||||
import { FrameProps, TransientFrameProps, withFrameProps } from '../common/frameProps';
|
||||
|
||||
type BadgeSize = Extract<UISize, 'tiny' | 'small' | 'medium'>;
|
||||
type BadgeVariant = Extract<UIVariant, 'primary' | 'tertiary' | 'destructive'>;
|
||||
|
||||
export interface BadgeProps {
|
||||
export type BadgeProps = FrameProps & {
|
||||
size?: BadgeSize;
|
||||
variant?: BadgeVariant;
|
||||
isDisabled?: boolean;
|
||||
@@ -17,7 +18,8 @@ export interface BadgeProps {
|
||||
hasAlert?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
inline?: boolean;
|
||||
};
|
||||
|
||||
type MapArgs = {
|
||||
$variant: BadgeVariant;
|
||||
@@ -28,7 +30,8 @@ type BadgeContainerProps = {
|
||||
$size: BadgeSize;
|
||||
$variant: BadgeVariant;
|
||||
$hasAlert: boolean;
|
||||
};
|
||||
$inline: boolean;
|
||||
} & TransientFrameProps;
|
||||
|
||||
const mapVariantToBackgroundColor = ({ $variant, theme }: MapArgs): CSSColor => {
|
||||
const colorMap: Record<BadgeVariant, Color> = {
|
||||
@@ -71,7 +74,9 @@ const mapVariantToPadding = ({ $size }: { $size: BadgeSize }): string => {
|
||||
};
|
||||
|
||||
const Container = styled.button<BadgeContainerProps>`
|
||||
display: flex;
|
||||
${withFrameProps}
|
||||
|
||||
display: ${({ $inline }) => ($inline ? 'inline-flex' : 'flex')};
|
||||
align-items: center;
|
||||
gap: ${spacingsPx.xxs};
|
||||
padding: ${mapVariantToPadding};
|
||||
@@ -110,6 +115,8 @@ export const Badge = ({
|
||||
hasAlert,
|
||||
className,
|
||||
children,
|
||||
inline,
|
||||
margin,
|
||||
}: BadgeProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -120,6 +127,8 @@ export const Badge = ({
|
||||
disabled={!!isDisabled}
|
||||
$hasAlert={!!hasAlert}
|
||||
className={className}
|
||||
$margin={margin}
|
||||
$inline={inline === true}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
|
||||
@@ -127,6 +127,21 @@ export const Dropdown: StoryObj<DropdownProps> = {
|
||||
argTypes: {
|
||||
addon: { control: { disable: true } },
|
||||
items: { control: { disable: true } },
|
||||
alignMenu: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: [
|
||||
'bottom-left',
|
||||
'bottom-right',
|
||||
'left-bottom',
|
||||
'left-top',
|
||||
'right-bottom',
|
||||
'right-top',
|
||||
'top-left',
|
||||
'top-right',
|
||||
],
|
||||
},
|
||||
content: { control: { disable: true } },
|
||||
className: { control: { disable: true } },
|
||||
coords: { control: { disable: true } },
|
||||
|
||||
@@ -30,16 +30,12 @@ type ColorProps = {
|
||||
theme: Colors;
|
||||
} & TransientProps<ExclusiveColorOrVariant>;
|
||||
|
||||
const getColorForTextVariant = ({
|
||||
$variant,
|
||||
theme,
|
||||
$color,
|
||||
}: ColorProps): CSSColor | 'inherit' | string => {
|
||||
const getColorForTextVariant = ({ $variant, theme, $color }: ColorProps): CSSColor | string => {
|
||||
if ($color !== undefined) {
|
||||
return $color;
|
||||
}
|
||||
|
||||
return $variant === undefined ? 'inherit' : theme[variantColorMap[$variant]];
|
||||
return theme[$variant !== undefined ? variantColorMap[$variant] : 'textDefault'];
|
||||
};
|
||||
|
||||
type StyledTextProps = {
|
||||
|
||||
@@ -4,7 +4,7 @@ export type OnboardingAnalytics = {
|
||||
startTime: number;
|
||||
firmware: 'install' | 'update' | 'skip' | 'up-to-date';
|
||||
seed: 'create' | 'recovery' | 'recovery-in-progress';
|
||||
seedType: 'standard' | 'shamir';
|
||||
seedType: 'shamir-default' | 'shamir-advance' | '12-words' | '24-words';
|
||||
recoveryType: 'standard' | 'advanced';
|
||||
backup: 'create' | 'skip';
|
||||
pin: 'create' | 'skip';
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@everstake/wallet-sdk": "^0.3.40",
|
||||
"@floating-ui/react": "^0.26.9",
|
||||
"@formatjs/intl": "2.10.0",
|
||||
"@hookform/resolvers": "3.3.4",
|
||||
"@mobily/ts-belt": "^3.13.1",
|
||||
|
||||
@@ -5062,12 +5062,65 @@ export default defineMessages({
|
||||
description:
|
||||
'Used for button triggering seed creation (reset device call), user chooses between single seed and shamir',
|
||||
},
|
||||
TR_ONBOARDING_GENERATE_SEED_DESCRIPTION: {
|
||||
id: 'TR_ONBOARDING_GENERATE_SEED_DESCRIPTION',
|
||||
TR_ONBOARDING_SELECT_SEED_TYPE: {
|
||||
id: 'TR_ONBOARDING_SELECT_SEED_TYPE',
|
||||
defaultMessage:
|
||||
'Choose how to back up your Trezor. This process will also create a standard wallet for you.',
|
||||
description:
|
||||
'Used for button triggering seed creation (reset device call), user chooses between single seed and shamir',
|
||||
'We have selected the <primary>optimal setup</primary> for backing up your Trezor.',
|
||||
},
|
||||
TR_ONBOARDING_BACKUP_TYPE: {
|
||||
id: 'TR_ONBOARDING_BACKUP_TYPE',
|
||||
defaultMessage: 'Backup Type',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_SINGLE_SEED: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_SINGLE_SEED',
|
||||
defaultMessage: 'Single seed of 20 words',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_SINGLE_SEED_DESCRIPTION: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_SINGLE_SEED_DESCRIPTION',
|
||||
defaultMessage:
|
||||
'Allows you to upgrade to multi later, if you decide you want more security.',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_ADVANCED: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_ADVANCED',
|
||||
defaultMessage: 'Multiple lists of 20 words',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_ADVANCED_DESCRIPTION: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_ADVANCED_DESCRIPTION',
|
||||
defaultMessage: 'Creates multiple pieces of backup. 20 words each.',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_12_WORDS: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_12_WORDS',
|
||||
defaultMessage: '12 words',
|
||||
},
|
||||
TR_ONBOARDING_SEED_TYPE_24_WORDS: {
|
||||
id: 'TR_ONBOARDING_SEED_TYPE_24_WORDS',
|
||||
defaultMessage: '24 words',
|
||||
},
|
||||
TR_ONBOARDING_BACKUP_TYPE_DEFAULT: {
|
||||
id: 'TR_ONBOARDING_BACKUP_TYPE_DEFAULT',
|
||||
defaultMessage: 'Default',
|
||||
},
|
||||
TR_ONBOARDING_BACKUP_TYPE_ADVANCED: {
|
||||
id: 'TR_ONBOARDING_BACKUP_TYPE_ADVANCED',
|
||||
defaultMessage: 'Advanced',
|
||||
},
|
||||
TR_ONBOARDING_BACKUP_OLDER_BACKUP_TYPES: {
|
||||
id: 'TR_ONBOARDING_BACKUP_OLDER_BACKUP_TYPES',
|
||||
defaultMessage:
|
||||
'Older backup types <br></br>These don’t allow to upgrade to multi-share later, if you decide you want more security.',
|
||||
},
|
||||
TR_ONBOARDING_BACKUP_LEGACY_WARNING: {
|
||||
id: 'TR_ONBOARDING_BACKUP_LEGACY_WARNING',
|
||||
defaultMessage:
|
||||
'This backup type won’t allow you to upgrade to multi backup later on if you decide you’d want more security. To allow for backup upgrade, use 20 words backup.',
|
||||
},
|
||||
TR_ONBOARDING_CANNOT_SELECT_SEED_TYPE: {
|
||||
id: 'TR_ONBOARDING_CANNOT_SELECT_SEED_TYPE',
|
||||
defaultMessage: 'Todo: user has no option, Trezor One is forced to use 24 words.',
|
||||
},
|
||||
TR_ONBOARDING_SELECT_SEED_TYPE_CONFIRM: {
|
||||
id: 'TR_ONBOARDING_SELECT_SEED_TYPE_CONFIRM',
|
||||
defaultMessage: 'Proceed',
|
||||
},
|
||||
TR_CREATE_WALLET: {
|
||||
id: 'TR_CREATE_WALLET',
|
||||
@@ -5075,25 +5128,6 @@ export default defineMessages({
|
||||
description:
|
||||
'Used for button triggering seed creation (reset device call) if shamir/non-shamir selection is not available.',
|
||||
},
|
||||
SINGLE_SEED: {
|
||||
id: 'SINGLE_SEED',
|
||||
defaultMessage: 'Standard seed backup',
|
||||
description: 'Basic, non-shamir backup. Seed has only one part.',
|
||||
},
|
||||
SINGLE_SEED_DESCRIPTION: {
|
||||
id: 'SINGLE_SEED_DESCRIPTION',
|
||||
defaultMessage: 'Recover your wallet using a single list of English words.',
|
||||
},
|
||||
SHAMIR_SEED: {
|
||||
id: 'SHAMIR_SEED',
|
||||
defaultMessage: 'Advanced Shamir Backup',
|
||||
description: 'Advanced, shamir backup. Seed has multiple parts.',
|
||||
},
|
||||
SHAMIR_SEED_DESCRIPTION: {
|
||||
id: 'SHAMIR_SEED_DESCRIPTION',
|
||||
defaultMessage:
|
||||
'Recover the wallet by combining lists of words together. These can be secured in different places for added security.',
|
||||
},
|
||||
TR_CHECK_FINGERPRINT: {
|
||||
id: 'TR_CHECK_FINGERPRINT',
|
||||
defaultMessage: 'Check fingerprint',
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Onboarding = () => {
|
||||
// Selection between a new seed or seed recovery
|
||||
return CreateOrRecover;
|
||||
case STEP.ID_RESET_DEVICE_STEP:
|
||||
// a) Generating a new seed, selection between single seed or shamir seed (only T2T1 supported)
|
||||
// a) Generating a new seed, selection between seed types
|
||||
return ResetDeviceStep;
|
||||
case STEP.ID_RECOVERY_STEP:
|
||||
// b) Seed recovery
|
||||
|
||||
@@ -1,111 +1,152 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { selectDevice } from '@suite-common/wallet-core';
|
||||
|
||||
import * as STEP from 'src/constants/onboarding/steps';
|
||||
import {
|
||||
OnboardingButtonBack,
|
||||
OnboardingOption,
|
||||
OptionsWrapper,
|
||||
OptionsDivider,
|
||||
OnboardingStepBox,
|
||||
} from 'src/components/onboarding';
|
||||
import { OnboardingButtonBack, OptionsWrapper, OnboardingStepBox } from 'src/components/onboarding';
|
||||
import { Translation } from 'src/components/suite';
|
||||
import { useDispatch, useSelector, useOnboarding, useDevice } from 'src/hooks/suite';
|
||||
import { resetDevice } from 'src/actions/settings/deviceSettingsActions';
|
||||
import { selectIsActionAbortable } from 'src/reducers/suite/suiteReducer';
|
||||
import { Button, Divider, Text } from '@trezor/components';
|
||||
import { BackupType, SelectBackupType, defaultBackupTypeMap } from './SelectBackupType';
|
||||
import { DeviceModelInternal } from '@trezor/connect';
|
||||
|
||||
const SelectWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const canChooseBackupType = (device: DeviceModelInternal) => device !== DeviceModelInternal.T1B1;
|
||||
|
||||
export const ResetDeviceStep = () => {
|
||||
const { isLocked } = useDevice();
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const { goToPreviousStep, goToNextStep, updateAnalytics } = useOnboarding();
|
||||
|
||||
const device = useSelector(selectDevice);
|
||||
const isActionAbortable = useSelector(selectIsActionAbortable);
|
||||
|
||||
const deviceModel = device?.features?.internal_model;
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [backupType, setBackupType] = useState<BackupType>(
|
||||
deviceModel !== undefined ? defaultBackupTypeMap[deviceModel] : 'shamir-default',
|
||||
);
|
||||
const { goToPreviousStep, goToNextStep, updateAnalytics } = useOnboarding();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isWaitingForConfirmation =
|
||||
device?.buttonRequests.some(
|
||||
r => r.code === 'ButtonRequest_ResetDevice' || r.code === 'ButtonRequest_ProtectCall',
|
||||
) && !submitted; // ButtonRequest_ResetDevice is for T2T1, ButtonRequest_ProtectCall for T1B1
|
||||
|
||||
const isDeviceLocked = isLocked();
|
||||
|
||||
const onResetDevice = useCallback(
|
||||
async (params?: Parameters<typeof resetDevice>[0]) => {
|
||||
setSubmitted(false);
|
||||
|
||||
const result = await dispatch(resetDevice(params));
|
||||
|
||||
setSubmitted(true);
|
||||
|
||||
if (result?.success) {
|
||||
goToNextStep(STEP.ID_SECURITY_STEP);
|
||||
}
|
||||
},
|
||||
[dispatch, goToNextStep],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (type: BackupType) => {
|
||||
switch (type) {
|
||||
case 'shamir-default':
|
||||
await onResetDevice({ backup_type: 1 }); // Todo: add number of shards = 1/1
|
||||
break;
|
||||
case 'shamir-advance':
|
||||
await onResetDevice({ backup_type: 1 }); // Todo: add number of shards = n/m (select on device?)
|
||||
break;
|
||||
case '12-words':
|
||||
await onResetDevice({ backup_type: 0, strength: 128 });
|
||||
break;
|
||||
case '24-words':
|
||||
await onResetDevice({ backup_type: 0, strength: 256 });
|
||||
break;
|
||||
}
|
||||
|
||||
updateAnalytics({ recoveryType: undefined, seedType: type });
|
||||
},
|
||||
[updateAnalytics, onResetDevice],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceModel !== undefined && !canChooseBackupType(deviceModel)) {
|
||||
handleSubmit(defaultBackupTypeMap[deviceModel]);
|
||||
}
|
||||
}, [deviceModel, handleSubmit]);
|
||||
|
||||
// this step expects device
|
||||
if (!device || !device.features) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShamirBackupAvailable = device.features?.capabilities?.includes('Capability_Shamir');
|
||||
const isWaitingForConfirmation =
|
||||
device.buttonRequests.some(
|
||||
r => r.code === 'ButtonRequest_ResetDevice' || r.code === 'ButtonRequest_ProtectCall',
|
||||
) && !submitted; // ButtonRequest_ResetDevice is for T2T1, ButtonRequest_ProtectCall for T1B1
|
||||
|
||||
const isDisabled = isLocked();
|
||||
|
||||
const onResetDevice = async (params?: { backup_type?: 0 | 1 | undefined }) => {
|
||||
setSubmitted(false);
|
||||
|
||||
const result = await dispatch(resetDevice(params));
|
||||
|
||||
setSubmitted(true);
|
||||
|
||||
if (result?.success) {
|
||||
goToNextStep(STEP.ID_SECURITY_STEP);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSingleSeedReset = async () => {
|
||||
if (isShamirBackupAvailable) {
|
||||
await onResetDevice({ backup_type: 0 });
|
||||
} else {
|
||||
await onResetDevice();
|
||||
}
|
||||
|
||||
updateAnalytics({ recoveryType: undefined, seedType: 'standard' });
|
||||
};
|
||||
|
||||
const handleShamirReset = async () => {
|
||||
await onResetDevice({ backup_type: 1 });
|
||||
|
||||
updateAnalytics({ recoveryType: undefined, seedType: 'shamir' });
|
||||
};
|
||||
const showSelect = !isWaitingForConfirmation && !isDeviceLocked;
|
||||
const canChoseBackupType = deviceModel !== undefined && canChooseBackupType(deviceModel);
|
||||
|
||||
return (
|
||||
<OnboardingStepBox
|
||||
image="KEY"
|
||||
heading={<Translation id="TR_ONBOARDING_GENERATE_SEED" />}
|
||||
description={<Translation id="TR_ONBOARDING_GENERATE_SEED_DESCRIPTION" />}
|
||||
description={
|
||||
canChoseBackupType ? (
|
||||
<Translation
|
||||
id="TR_ONBOARDING_SELECT_SEED_TYPE"
|
||||
values={{
|
||||
primary: chunks => <Text variant="primary">{chunks}</Text>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Translation id="TR_ONBOARDING_CANNOT_SELECT_SEED_TYPE" />
|
||||
)
|
||||
}
|
||||
device={isWaitingForConfirmation ? device : undefined}
|
||||
isActionAbortable={isActionAbortable}
|
||||
outerActions={
|
||||
!isWaitingForConfirmation ? (
|
||||
showSelect && (
|
||||
// There is no point to show back button if user can't click it because confirmOnDevice bubble is active
|
||||
<OnboardingButtonBack onClick={() => goToPreviousStep()} />
|
||||
) : undefined
|
||||
)
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
{!isWaitingForConfirmation ? (
|
||||
// Show options to chose from only if we are not waiting for confirmation on the device (because that means user has already chosen )
|
||||
<OptionsWrapper $fullWidth={false}>
|
||||
<OnboardingOption
|
||||
icon="SEED_SINGLE"
|
||||
data-test={
|
||||
isShamirBackupAvailable
|
||||
? '@onboarding/button-standard-backup'
|
||||
: '@onboarding/only-backup-option-button'
|
||||
}
|
||||
onClick={isDisabled ? undefined : handleSingleSeedReset}
|
||||
heading={<Translation id="SINGLE_SEED" />}
|
||||
description={<Translation id="SINGLE_SEED_DESCRIPTION" />}
|
||||
/>
|
||||
|
||||
{isShamirBackupAvailable && (
|
||||
<>
|
||||
<OptionsDivider />
|
||||
<OnboardingOption
|
||||
icon="SEED_SHAMIR"
|
||||
data-test="@onboarding/shamir-backup-option-button"
|
||||
onClick={isDisabled ? undefined : handleShamirReset}
|
||||
heading={<Translation id="SHAMIR_SEED" />}
|
||||
description={<Translation id="SHAMIR_SEED_DESCRIPTION" />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showSelect ? (
|
||||
<OptionsWrapper $fullWidth={true}>
|
||||
<SelectWrapper>
|
||||
{canChoseBackupType && (
|
||||
<>
|
||||
<SelectBackupType
|
||||
selected={backupType}
|
||||
onSelect={setBackupType}
|
||||
isDisabled={isDeviceLocked}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<ButtonWrapper>
|
||||
<Button
|
||||
variant="primary"
|
||||
isDisabled={isDeviceLocked}
|
||||
onClick={() => handleSubmit(backupType)}
|
||||
>
|
||||
<Translation id="TR_ONBOARDING_SELECT_SEED_TYPE_CONFIRM" />
|
||||
</Button>
|
||||
</ButtonWrapper>
|
||||
</SelectWrapper>
|
||||
</OptionsWrapper>
|
||||
) : undefined}
|
||||
</OnboardingStepBox>
|
||||
|
||||
348
packages/suite/src/views/onboarding/steps/SelectBackupType.tsx
Normal file
348
packages/suite/src/views/onboarding/steps/SelectBackupType.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
Badge,
|
||||
Divider,
|
||||
ElevationUp,
|
||||
Icon,
|
||||
Radio,
|
||||
Text,
|
||||
Warning,
|
||||
useElevation,
|
||||
} from '@trezor/components';
|
||||
import {
|
||||
Elevation,
|
||||
borders,
|
||||
mapElevationToBackground,
|
||||
mapElevationToBorder,
|
||||
spacings,
|
||||
spacingsPx,
|
||||
zIndices,
|
||||
} from '@trezor/theme';
|
||||
import { CSSProperties, ReactNode, forwardRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from '../../../hooks/suite';
|
||||
import { selectDevice } from '@suite-common/wallet-core';
|
||||
import { DeviceModelInternal } from '@trezor/connect';
|
||||
import {
|
||||
FloatingFocusManager,
|
||||
autoUpdate,
|
||||
useFloating,
|
||||
useClick,
|
||||
useDismiss,
|
||||
useRole,
|
||||
useInteractions,
|
||||
FloatingPortal,
|
||||
size,
|
||||
offset,
|
||||
} from '@floating-ui/react';
|
||||
import { TranslationKey } from '@suite-common/intl-types';
|
||||
import { Translation } from '../../../components/suite';
|
||||
|
||||
export const selectBackupTypes = [
|
||||
'shamir-default',
|
||||
'shamir-advance',
|
||||
'12-words',
|
||||
'24-words',
|
||||
] as const;
|
||||
|
||||
export type BackupType = (typeof selectBackupTypes)[number];
|
||||
|
||||
const isDeviceLegacyMap: Record<DeviceModelInternal, boolean> = {
|
||||
[DeviceModelInternal.T1B1]: true,
|
||||
[DeviceModelInternal.T2T1]: true,
|
||||
[DeviceModelInternal.T2B1]: false,
|
||||
[DeviceModelInternal.T3T1]: false
|
||||
};
|
||||
|
||||
const isLegacyDevice = (model?: DeviceModelInternal) =>
|
||||
model !== undefined && isDeviceLegacyMap[model];
|
||||
|
||||
export const isShamirBackupType = (type: BackupType) =>
|
||||
['shamir-default', 'shamir-advance'].includes(type);
|
||||
|
||||
const typesToLabelMap: Record<BackupType, TranslationKey> = {
|
||||
'shamir-default': 'TR_ONBOARDING_SEED_TYPE_SINGLE_SEED',
|
||||
'shamir-advance': 'TR_ONBOARDING_SEED_TYPE_ADVANCED',
|
||||
'12-words': 'TR_ONBOARDING_SEED_TYPE_12_WORDS',
|
||||
'24-words': 'TR_ONBOARDING_SEED_TYPE_24_WORDS',
|
||||
};
|
||||
|
||||
export const defaultBackupTypeMap: Record<DeviceModelInternal, BackupType> = {
|
||||
[DeviceModelInternal.T1B1]: '24-words',
|
||||
[DeviceModelInternal.T2T1]: '12-words',
|
||||
[DeviceModelInternal.T2B1]: 'shamir-default',
|
||||
[DeviceModelInternal.T3T1]: 'shamir-default'
|
||||
};
|
||||
|
||||
const SELECT_ELEMENT_HEIGHT = 84;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${spacingsPx.xl};
|
||||
`;
|
||||
|
||||
const SelectWrapper = styled.div<{ $elevation: Elevation }>`
|
||||
width: 100%;
|
||||
border-radius: ${borders.radii.sm};
|
||||
border: 1px solid ${mapElevationToBorder};
|
||||
background: ${mapElevationToBackground};
|
||||
height: ${SELECT_ELEMENT_HEIGHT}px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const OptionText = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const DownIconCircle = styled.div<{ $elevation: Elevation }>`
|
||||
border-radius: ${borders.radii.full};
|
||||
border: 1px solid ${mapElevationToBorder};
|
||||
background: ${mapElevationToBackground};
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const BackupIconWrapper = styled.div``;
|
||||
|
||||
const OptionStyled = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${spacingsPx.md} ${spacingsPx.xl};
|
||||
gap: ${spacingsPx.md};
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const DownComponent = () => {
|
||||
const { elevation } = useElevation();
|
||||
|
||||
return (
|
||||
<DownIconCircle $elevation={elevation}>
|
||||
<Icon icon="ARROW_DOWN" size={16} />
|
||||
</DownIconCircle>
|
||||
);
|
||||
};
|
||||
|
||||
type SelectedOptionProps = { children: ReactNode; onClick: () => void; isDisabled: boolean };
|
||||
|
||||
const SelectedOptionStyled = styled.div<{ $isDisabled: boolean }>`
|
||||
cursor: ${({ $isDisabled }) => ($isDisabled ? undefined : 'pointer')};
|
||||
`;
|
||||
|
||||
const SelectedOption = forwardRef<HTMLDivElement, SelectedOptionProps>(
|
||||
({ children, onClick, isDisabled }, ref) => (
|
||||
<SelectedOptionStyled $isDisabled={isDisabled}>
|
||||
<OptionStyled ref={ref} onClick={isDisabled ? undefined : onClick}>
|
||||
<BackupIconWrapper>
|
||||
<Icon icon="BACKUP" size={24} />
|
||||
</BackupIconWrapper>
|
||||
{children}
|
||||
<DownComponent />
|
||||
</OptionStyled>
|
||||
</SelectedOptionStyled>
|
||||
),
|
||||
);
|
||||
|
||||
type OptionProps = {
|
||||
children: ReactNode;
|
||||
onSelect: () => void;
|
||||
isChecked: boolean;
|
||||
};
|
||||
|
||||
const Option = ({ children, onSelect, isChecked }: OptionProps) => (
|
||||
<OptionStyled>
|
||||
{children}
|
||||
<Radio isChecked={isChecked} onClick={onSelect} />
|
||||
</OptionStyled>
|
||||
);
|
||||
|
||||
const FloatingSelectionsWrapper = styled.div<{ $elevation: Elevation }>`
|
||||
z-index: ${zIndices.modal};
|
||||
border-radius: ${borders.radii.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadowElevated};
|
||||
background: ${mapElevationToBackground};
|
||||
`;
|
||||
|
||||
type FloatingSelectionsProps = {
|
||||
selected: BackupType;
|
||||
onSelect: (value: BackupType) => void;
|
||||
style: CSSProperties;
|
||||
};
|
||||
|
||||
const FloatingSelections = forwardRef<HTMLDivElement, FloatingSelectionsProps>(
|
||||
({ selected, onSelect, style }, ref) => {
|
||||
const { elevation } = useElevation();
|
||||
|
||||
return (
|
||||
<FloatingSelectionsWrapper $elevation={elevation} ref={ref} style={style}>
|
||||
<Option
|
||||
onSelect={() => onSelect('shamir-default')}
|
||||
isChecked={selected === 'shamir-default'}
|
||||
>
|
||||
<OptionText>
|
||||
<Text variant="tertiary" typographyStyle="titleSmall">
|
||||
<Translation id={typesToLabelMap['shamir-default']} />
|
||||
<Badge variant="primary" inline margin={{ left: spacings.xxs }}>
|
||||
<Text typographyStyle="hint">
|
||||
<Translation id="TR_ONBOARDING_BACKUP_TYPE_DEFAULT" />
|
||||
</Text>
|
||||
</Badge>
|
||||
</Text>
|
||||
<Text typographyStyle="hint">
|
||||
<Translation id="TR_ONBOARDING_SEED_TYPE_SINGLE_SEED_DESCRIPTION" />
|
||||
</Text>
|
||||
</OptionText>
|
||||
</Option>
|
||||
<Option
|
||||
onSelect={() => onSelect('shamir-advance')}
|
||||
isChecked={selected === 'shamir-advance'}
|
||||
>
|
||||
<OptionText>
|
||||
<Text variant={'tertiary'} typographyStyle="titleSmall">
|
||||
<Translation id={typesToLabelMap['shamir-advance']} />
|
||||
<Badge variant="tertiary" inline margin={{ left: spacings.xxs }}>
|
||||
<Translation id="TR_ONBOARDING_BACKUP_TYPE_ADVANCED" />
|
||||
</Badge>
|
||||
</Text>
|
||||
<Text typographyStyle="hint">
|
||||
<Translation id="TR_ONBOARDING_SEED_TYPE_SINGLE_SEED_DESCRIPTION" />
|
||||
</Text>
|
||||
</OptionText>
|
||||
</Option>
|
||||
<Divider />
|
||||
<OptionStyled>
|
||||
<Text typographyStyle="hint">
|
||||
<Translation
|
||||
id="TR_ONBOARDING_BACKUP_OLDER_BACKUP_TYPES"
|
||||
values={{ br: () => <br /> }}
|
||||
/>
|
||||
</Text>
|
||||
</OptionStyled>
|
||||
<Option onSelect={() => onSelect('12-words')} isChecked={selected === '12-words'}>
|
||||
<OptionText>
|
||||
<Text variant={'tertiary'} typographyStyle="titleSmall">
|
||||
<Translation id={typesToLabelMap['12-words']} />
|
||||
</Text>
|
||||
</OptionText>
|
||||
</Option>
|
||||
<Option onSelect={() => onSelect('24-words')} isChecked={selected === '24-words'}>
|
||||
<OptionText>
|
||||
<Text variant={'tertiary'} typographyStyle="titleSmall">
|
||||
<Translation id={typesToLabelMap['24-words']} />
|
||||
</Text>
|
||||
</OptionText>
|
||||
</Option>
|
||||
</FloatingSelectionsWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const LegacyWarning = () => {
|
||||
return (
|
||||
<Warning variant="info" withIcon>
|
||||
<Translation id="TR_ONBOARDING_BACKUP_LEGACY_WARNING" />
|
||||
</Warning>
|
||||
);
|
||||
};
|
||||
|
||||
type SelectBackupTypeProps = {
|
||||
isDisabled: boolean;
|
||||
selected: BackupType;
|
||||
onSelect: (value: BackupType) => void;
|
||||
};
|
||||
|
||||
export const SelectBackupType = ({ selected, onSelect, isDisabled }: SelectBackupTypeProps) => {
|
||||
const { elevation } = useElevation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const device = useSelector(selectDevice);
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
middleware: [
|
||||
offset(-SELECT_ELEMENT_HEIGHT),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
});
|
||||
},
|
||||
padding: 10,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
const role = useRole(context);
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
|
||||
|
||||
const defaultBackupType: BackupType = device?.features?.internal_model
|
||||
? defaultBackupTypeMap[device.features.internal_model]
|
||||
: 'shamir-default';
|
||||
|
||||
// We want to show user the 12-words because its default for this device,
|
||||
// and we want to show user this explicit information, not just default as it is
|
||||
// for newer devices.
|
||||
const isModelT = device?.features?.internal_model === DeviceModelInternal.T2T1;
|
||||
|
||||
const isDefault = defaultBackupType === selected && !isModelT;
|
||||
const isShamirSelected = isShamirBackupType(selected);
|
||||
const isLegacyModel = isLegacyDevice(device?.features?.internal_model);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<SelectWrapper $elevation={elevation}>
|
||||
<ElevationUp>
|
||||
<SelectedOption
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => setIsOpen(true)}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
<OptionText>
|
||||
<Text variant="tertiary" typographyStyle="hint">
|
||||
<Translation id="TR_ONBOARDING_BACKUP_TYPE" />
|
||||
</Text>
|
||||
<Text typographyStyle="titleSmall">
|
||||
<Translation
|
||||
id={
|
||||
isDefault
|
||||
? 'TR_ONBOARDING_BACKUP_TYPE_DEFAULT'
|
||||
: typesToLabelMap[selected]
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
</OptionText>
|
||||
</SelectedOption>
|
||||
{isOpen && (
|
||||
<FloatingPortal>
|
||||
<FloatingFocusManager context={context} modal={false}>
|
||||
<FloatingSelections
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
selected={selected}
|
||||
onSelect={value => {
|
||||
setIsOpen(false);
|
||||
onSelect(value);
|
||||
}}
|
||||
/>
|
||||
</FloatingFocusManager>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</ElevationUp>
|
||||
</SelectWrapper>
|
||||
{!isShamirSelected && !isLegacyModel && <LegacyWarning />}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user