feat(suite): refactor of the backup type

feat(suite): use FloatingUI to do the dropdown
This commit is contained in:
Peter Sanderson
2024-04-03 15:20:31 +02:00
committed by Peter Sanderson
parent be86c395ad
commit 0549ee8557
10 changed files with 559 additions and 114 deletions

View File

@@ -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

View File

@@ -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 } },

View File

@@ -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 = {

View File

@@ -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';

View File

@@ -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",

View File

@@ -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 dont 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 wont allow you to upgrade to multi backup later on if you decide youd 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',

View File

@@ -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

View File

@@ -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>

View 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>
);
};

View File

@@ -11157,6 +11157,7 @@ __metadata:
dependencies:
"@crowdin/cli": "npm:^3.18.0"
"@everstake/wallet-sdk": "npm:^0.3.40"
"@floating-ui/react": "npm:^0.26.9"
"@formatjs/cli": "npm:^6.2.7"
"@formatjs/intl": "npm:2.10.0"
"@hookform/resolvers": "npm:3.3.4"