mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-03 05:55:03 +01:00
feat(suite-native): Settings: Change Device Name
This commit is contained in:
committed by
Bohdan Juříček
parent
c956709c65
commit
d67742d376
@@ -1,5 +1,7 @@
|
||||
// Regular expression to match non-ASCII characters
|
||||
const nonAsciiPattern = /[^\x20-\x7E]/g;
|
||||
const ASCII_RANGE = '[^\x20-\x7E]';
|
||||
|
||||
const nonAsciiPattern = new RegExp(ASCII_RANGE);
|
||||
const nonAsciiPatternGlobal = new RegExp(ASCII_RANGE, 'g');
|
||||
|
||||
export function isAscii(value?: string): boolean {
|
||||
if (!value) return true;
|
||||
@@ -10,5 +12,5 @@ export function isAscii(value?: string): boolean {
|
||||
export function getNonAsciiChars(value?: string): RegExpMatchArray | null {
|
||||
if (!value) return null;
|
||||
|
||||
return value.match(nonAsciiPattern);
|
||||
return value.match(nonAsciiPatternGlobal);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ const redirectToDeviceAuthenticityScreenButton = element(
|
||||
);
|
||||
|
||||
class DeviceSettingsActions {
|
||||
async waitForSettingsScreen() {
|
||||
await waitFor(element(by.id('@screen/DeviceSettings')))
|
||||
.toBeVisible()
|
||||
.withTimeout(10000);
|
||||
}
|
||||
|
||||
async waitForPinProtectionScreen() {
|
||||
await waitFor(element(by.id('@screen/PinProtection')))
|
||||
.toBeVisible()
|
||||
@@ -57,6 +63,23 @@ class DeviceSettingsActions {
|
||||
await scrollUntilVisible(redirectToDeviceAuthenticityScreenButton);
|
||||
}
|
||||
|
||||
async tapChangeDeviceNameButton() {
|
||||
const changeDeviceNameButton = element(by.id('@device-name/change-button'));
|
||||
|
||||
await waitFor(changeDeviceNameButton).toBeVisible().withTimeout(10000);
|
||||
await changeDeviceNameButton.tap();
|
||||
}
|
||||
|
||||
async submitNewDeviceName(value: string) {
|
||||
const changeDeviceNameInput = element(by.id('@device-name/input'));
|
||||
const changeDeviceNameSubmitButton = element(by.id('@device-name/submit-button'));
|
||||
|
||||
await waitFor(changeDeviceNameInput).toBeVisible().withTimeout(10000);
|
||||
await changeDeviceNameInput.tap();
|
||||
await changeDeviceNameInput.replaceText(value);
|
||||
await changeDeviceNameSubmitButton.tap();
|
||||
}
|
||||
|
||||
async tapCheckAuthenticityButton() {
|
||||
const checkDeviceAuthenticityButton = element(by.id('@device-authenticity/check-button'));
|
||||
await waitFor(checkDeviceAuthenticityButton).toBeVisible().withTimeout(5_000);
|
||||
|
||||
@@ -91,4 +91,14 @@ conditionalDescribe(device.getPlatform() === 'android', 'Device settings', () =>
|
||||
|
||||
await onDeviceSettings.waitForDeviceAuthenticityScreen();
|
||||
});
|
||||
|
||||
test('Change Device Name', async () => {
|
||||
await onDeviceSettings.tapChangeDeviceNameButton();
|
||||
await onDeviceSettings.submitNewDeviceName('new name');
|
||||
await TrezorUserEnvLink.pressYes();
|
||||
|
||||
await onDeviceSettings.waitForSettingsScreen();
|
||||
|
||||
expect(element(by.label('new name'))).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ export type InputProps = TextInputProps &
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
elevation?: SurfaceElevation;
|
||||
keepPlaceholderOnFocus?: boolean;
|
||||
},
|
||||
'label' | 'placeholder'
|
||||
>;
|
||||
@@ -244,10 +245,11 @@ export const Input = forwardRef<TextInput, InputProps>(
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
style,
|
||||
editable,
|
||||
hasError = false,
|
||||
hasWarning = false,
|
||||
elevation = '0',
|
||||
editable,
|
||||
keepPlaceholderOnFocus = false,
|
||||
...props
|
||||
}: InputProps,
|
||||
ref,
|
||||
@@ -274,6 +276,10 @@ export const Input = forwardRef<TextInput, InputProps>(
|
||||
onBlur?.(event);
|
||||
};
|
||||
|
||||
const shouldShowPlaceholder = keepPlaceholderOnFocus
|
||||
? S.isEmpty(value)
|
||||
: !isFocused && S.isEmpty(value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@@ -310,7 +316,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
||||
{label}
|
||||
</Animated.Text>
|
||||
)}
|
||||
{!isFocused && S.isEmpty(value) && placeholder && (
|
||||
{shouldShowPlaceholder && placeholder && (
|
||||
<Animated.View
|
||||
entering={labelEnteringAnimation}
|
||||
exiting={labelExitingAnimation}
|
||||
|
||||
@@ -21,8 +21,9 @@ const labelStyle = prepareNativeStyle(utils => ({
|
||||
}));
|
||||
|
||||
const hintStyle = prepareNativeStyle(
|
||||
(_, { error, hint }: Pick<InputWrapperProps, 'error' | 'hint'>) => ({
|
||||
(utils, { error, hint }: Pick<InputWrapperProps, 'error' | 'hint'>) => ({
|
||||
marginTop: 0,
|
||||
marginLeft: utils.spacings.sp12,
|
||||
extend: {
|
||||
condition: !!error || !!hint,
|
||||
style: {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"react": "19.0.0",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-native": "0.79.3",
|
||||
"react-native-reanimated": "^3.18.0",
|
||||
"yup": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
40
suite-native/forms/src/components/FormSubmitButton.tsx
Normal file
40
suite-native/forms/src/components/FormSubmitButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ComponentProps, PropsWithChildren } from 'react';
|
||||
import Animated, {
|
||||
SlideInDown,
|
||||
SlideOutDown,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { Button } from '@suite-native/atoms';
|
||||
|
||||
type FormSubmitButtonProps = PropsWithChildren<{
|
||||
isVisible: boolean;
|
||||
}> &
|
||||
ComponentProps<typeof Button>;
|
||||
|
||||
export const FormSubmitButton = ({
|
||||
isVisible,
|
||||
onPress,
|
||||
children,
|
||||
...restProps
|
||||
}: FormSubmitButtonProps) => {
|
||||
const animatedButtonContainerStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
height: withTiming(isVisible ? 50 : 0),
|
||||
}),
|
||||
[isVisible],
|
||||
);
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedButtonContainerStyle}>
|
||||
{isVisible && (
|
||||
<Animated.View entering={SlideInDown} exiting={SlideOutDown}>
|
||||
<Button onPress={onPress} {...restProps}>
|
||||
{children}
|
||||
</Button>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -5,3 +5,4 @@ export * from './types';
|
||||
export * from './hooks/useForm';
|
||||
export * from './hooks/useFormContext';
|
||||
export * from './hooks/useField';
|
||||
export * from './components/FormSubmitButton';
|
||||
|
||||
@@ -549,6 +549,18 @@ export const en = {
|
||||
checks: 'Checks',
|
||||
dangerZone: 'Danger Zone',
|
||||
},
|
||||
changeDeviceName: {
|
||||
title: `Rename your Trezor`,
|
||||
validations: {
|
||||
noSpecialCharacters: 'Your Trezor’s name can’t contain special characters',
|
||||
maxLengthInfo: 'The name can be 16 characters long at most',
|
||||
englishLettersOnly: 'Your Trezor’s name can only contain english letters',
|
||||
},
|
||||
submitButton: 'Confirm',
|
||||
loadingSuccessScreen: {
|
||||
title: 'Name changed!',
|
||||
},
|
||||
},
|
||||
pinProtection: {
|
||||
title: 'PIN protection',
|
||||
content: 'PIN protects your device against physical attack.',
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@trezor/connect": "workspace:*",
|
||||
"@trezor/device-utils": "workspace:*",
|
||||
"@trezor/styles": "workspace:*",
|
||||
"@trezor/utils": "workspace:*",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.3",
|
||||
"react-redux": "9.2.0"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { selectHasRunningDiscovery } from '@suite-common/wallet-core';
|
||||
import { HStack, IconButton, Text, VStack } from '@suite-native/atoms';
|
||||
import { DeviceImage } from '@suite-native/device';
|
||||
import { useIsMultiline } from '@suite-native/helpers';
|
||||
import {
|
||||
DeviceSettingsStackParamList,
|
||||
DeviceSettingsStackRoutes,
|
||||
StackNavigationProps,
|
||||
} from '@suite-native/navigation';
|
||||
import { DeviceModelInternal } from '@trezor/device-utils';
|
||||
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
|
||||
|
||||
type DeviceInfoProps = {
|
||||
deviceModel: DeviceModelInternal;
|
||||
deviceName: string;
|
||||
};
|
||||
|
||||
type NavigationProp = StackNavigationProps<
|
||||
DeviceSettingsStackParamList,
|
||||
DeviceSettingsStackRoutes.DeviceSettings
|
||||
>;
|
||||
|
||||
const textStyle = prepareNativeStyle(_utils => ({
|
||||
lineHeight: undefined, // Reset line height to default, without this the text cannot align properly
|
||||
maxWidth: '90%',
|
||||
}));
|
||||
|
||||
export const DeviceInfo = ({ deviceModel, deviceName }: DeviceInfoProps) => {
|
||||
const isDiscoveryRunning = useSelector(selectHasRunningDiscovery);
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { applyStyle } = useNativeStyles();
|
||||
const { onTextLayout, isMultiline } = useIsMultiline();
|
||||
|
||||
const navigateToDeviceNameStack = () => {
|
||||
navigation.navigate(DeviceSettingsStackRoutes.DeviceNameStack);
|
||||
};
|
||||
|
||||
const name = isMultiline ? deviceName.replace(' ', '\n') : deviceName;
|
||||
|
||||
return (
|
||||
<VStack marginTop="sp24" spacing="sp24" alignItems="center">
|
||||
<DeviceImage deviceModel={deviceModel} />
|
||||
<HStack alignItems="center" spacing="sp12">
|
||||
<Text style={applyStyle(textStyle)} variant="titleMedium" onLayout={onTextLayout}>
|
||||
{name}
|
||||
</Text>
|
||||
<IconButton
|
||||
onPress={navigateToDeviceNameStack}
|
||||
isLoading={isDiscoveryRunning}
|
||||
testID="@device-name/change-button"
|
||||
size="extraSmall"
|
||||
iconName="pencilSimpleLine"
|
||||
colorScheme="tertiaryElevation0"
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { yup } from '@suite-common/validators';
|
||||
import { useTranslate } from '@suite-native/intl';
|
||||
import { isAscii } from '@trezor/utils';
|
||||
|
||||
const noSpecialCharacter = /^(?!.*[\p{M}\p{Lm}])[\x20-\x7E\p{L}\p{N}'-]+$/u;
|
||||
|
||||
export const deviceNameFormValidationSchema = (t: ReturnType<typeof useTranslate>['translate']) =>
|
||||
yup.object({
|
||||
deviceName: yup
|
||||
.string()
|
||||
.test({
|
||||
test: (value?: string) => {
|
||||
if (!value || isAscii(value)) return true;
|
||||
|
||||
return noSpecialCharacter.test(value);
|
||||
},
|
||||
message: t('moduleDeviceSettings.changeDeviceName.validations.noSpecialCharacters'),
|
||||
})
|
||||
.test({
|
||||
test: isAscii,
|
||||
message: t('moduleDeviceSettings.changeDeviceName.validations.englishLettersOnly'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { selectSelectedDevice } from '@suite-common/wallet-core';
|
||||
import { useForm } from '@suite-native/forms';
|
||||
import { useTranslate } from '@suite-native/intl';
|
||||
import {
|
||||
DeviceNameStackParamList,
|
||||
DeviceNameStackRoutes,
|
||||
StackNavigationProps,
|
||||
} from '@suite-native/navigation';
|
||||
import TrezorConnect from '@trezor/connect';
|
||||
import { EventType, analytics } from '@trezor/suite-analytics';
|
||||
|
||||
import { deviceNameFormValidationSchema } from '../deviceNameFormSchema';
|
||||
|
||||
export const MAX_LENGTH = 16;
|
||||
|
||||
type NavigationProps = StackNavigationProps<
|
||||
DeviceNameStackParamList,
|
||||
DeviceNameStackRoutes.DeviceName
|
||||
>;
|
||||
|
||||
export const useChangeDeviceName = () => {
|
||||
const { translate } = useTranslate();
|
||||
const navigation = useNavigation<NavigationProps>();
|
||||
const device = useSelector(selectSelectedDevice);
|
||||
|
||||
const form = useForm({
|
||||
validation: deviceNameFormValidationSchema(translate),
|
||||
defaultValues: {
|
||||
deviceName: device?.label || '',
|
||||
},
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
});
|
||||
|
||||
const deviceNameError = form.formState.errors.deviceName;
|
||||
const deviceNameValue = form.watch('deviceName');
|
||||
const isMaxLengthReached = deviceNameValue.length >= MAX_LENGTH;
|
||||
const isSubmittable = form.formState.isValid && deviceNameValue.length <= MAX_LENGTH;
|
||||
|
||||
const hintMessage =
|
||||
!deviceNameError &&
|
||||
isMaxLengthReached &&
|
||||
translate('moduleDeviceSettings.changeDeviceName.validations.maxLengthInfo');
|
||||
|
||||
const onSubmit = form.handleSubmit(async ({ deviceName }) => {
|
||||
if (!device) return;
|
||||
|
||||
const label = deviceName.length === 0 ? device.name : deviceName;
|
||||
if (deviceName === device.label) {
|
||||
navigation.goBack();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
navigation.navigate(DeviceNameStackRoutes.ContinueOnTrezor);
|
||||
const response = await TrezorConnect.applySettings({
|
||||
device: {
|
||||
path: device.path,
|
||||
},
|
||||
label,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
navigation.goBack();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
navigation.navigate(DeviceNameStackRoutes.DeviceNameLoadingScreen);
|
||||
|
||||
analytics.report({
|
||||
type: EventType.SettingsDeviceChangeLabel,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
deviceNameValue,
|
||||
deviceNameError,
|
||||
isSubmittable,
|
||||
hintMessage,
|
||||
device,
|
||||
onSubmit,
|
||||
isMaxLengthReached,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DeviceNameStackParamList,
|
||||
DeviceNameStackRoutes,
|
||||
stackNavigationOptionsConfig,
|
||||
} from '@suite-native/navigation';
|
||||
|
||||
import { useDeviceConnectionGuard } from '../hooks/useDeviceConnectionGuard';
|
||||
import { ContinueOnTrezorScreen } from '../screens/ContinueOnTrezorScreen';
|
||||
import { DeviceNameLoadingScreen } from '../screens/DeviceNameLoadingScreen';
|
||||
import { DeviceNameScreen } from '../screens/DeviceNameScreen';
|
||||
|
||||
const DeviceNameStack = createNativeStackNavigator<DeviceNameStackParamList>();
|
||||
|
||||
export const DeviceNameStackNavigator = () => {
|
||||
const { isDeviceConnected } = useDeviceConnectionGuard();
|
||||
|
||||
if (!isDeviceConnected) return null;
|
||||
|
||||
return (
|
||||
<DeviceNameStack.Navigator
|
||||
initialRouteName={DeviceNameStackRoutes.DeviceName}
|
||||
screenOptions={stackNavigationOptionsConfig}
|
||||
>
|
||||
<DeviceNameStack.Screen
|
||||
name={DeviceNameStackRoutes.DeviceName}
|
||||
component={DeviceNameScreen}
|
||||
/>
|
||||
<DeviceNameStack.Screen
|
||||
name={DeviceNameStackRoutes.ContinueOnTrezor}
|
||||
component={ContinueOnTrezorScreen}
|
||||
/>
|
||||
<DeviceNameStack.Screen
|
||||
name={DeviceNameStackRoutes.DeviceNameLoadingScreen}
|
||||
component={DeviceNameLoadingScreen}
|
||||
/>
|
||||
</DeviceNameStack.Navigator>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@suite-native/navigation';
|
||||
|
||||
import { DeviceAuthenticityStackNavigator } from './DeviceAuthenticityStackNavigator';
|
||||
import { DeviceNameStackNavigator } from './DeviceNameStackNavigator';
|
||||
import { DevicePinProtectionStackNavigator } from './DevicePinProtectionStackNavigator';
|
||||
import { WipeDeviceStackNavigator } from './WipeDeviceStackNavigator';
|
||||
import { ConfirmFirmwareUpdateScreen } from '../screens/ConfirmFirmwareUpdateScreen';
|
||||
@@ -59,5 +60,9 @@ export const DeviceSettingsStackNavigator = () => (
|
||||
name={DeviceSettingsStackRoutes.FirmwareInstallation}
|
||||
component={FirmwareInstallationScreen}
|
||||
/>
|
||||
<DeviceSettingsStack.Screen
|
||||
name={DeviceSettingsStackRoutes.DeviceNameStack}
|
||||
component={DeviceNameStackNavigator}
|
||||
/>
|
||||
</DeviceSettingsStack.Navigator>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { Translation } from '@suite-native/intl';
|
||||
import {
|
||||
DeviceSettingsStackParamList,
|
||||
DeviceSettingsStackRoutes,
|
||||
LoadingSuccessScreen,
|
||||
RootStackParamList,
|
||||
StackToStackCompositeNavigationProps,
|
||||
} from '@suite-native/navigation';
|
||||
|
||||
type NavigationProps = StackToStackCompositeNavigationProps<
|
||||
DeviceSettingsStackParamList,
|
||||
DeviceSettingsStackRoutes,
|
||||
RootStackParamList
|
||||
>;
|
||||
|
||||
export const DeviceNameLoadingScreen = () => {
|
||||
const navigation = useNavigation<NavigationProps>();
|
||||
|
||||
const handleFinish = () => {
|
||||
navigation.navigate(DeviceSettingsStackRoutes.DeviceSettings);
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingSuccessScreen
|
||||
onFinish={handleFinish}
|
||||
title={
|
||||
<Translation id="moduleDeviceSettings.changeDeviceName.loadingSuccessScreen.title" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Text, TitleHeader, VStack } from '@suite-native/atoms';
|
||||
import { Form, FormSubmitButton, TextInputField } from '@suite-native/forms';
|
||||
import { Translation } from '@suite-native/intl';
|
||||
import { Screen, ScreenHeader } from '@suite-native/navigation';
|
||||
|
||||
import { MAX_LENGTH, useChangeDeviceName } from '../hooks/useChangeDeviceName';
|
||||
|
||||
export const DeviceNameScreen = () => {
|
||||
const { form, device, hintMessage, deviceNameValue, onSubmit, isSubmittable } =
|
||||
useChangeDeviceName();
|
||||
|
||||
return (
|
||||
<Screen header={<ScreenHeader closeActionType="close" />}>
|
||||
<Form form={form}>
|
||||
<VStack marginTop="sp32" spacing="sp32" style={{ marginBottom: 'auto' }}>
|
||||
<TitleHeader
|
||||
title={<Translation id="moduleDeviceSettings.changeDeviceName.title" />}
|
||||
titleVariant="titleMedium"
|
||||
/>
|
||||
<VStack>
|
||||
<TextInputField
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
keepPlaceholderOnFocus
|
||||
placeholder={device?.name || ''}
|
||||
name="deviceName"
|
||||
autoCorrect={false}
|
||||
keyboardType="ascii-capable"
|
||||
testID="@device-name/input"
|
||||
accessibilityLabel="device name input"
|
||||
hint={hintMessage || ''}
|
||||
maxLength={MAX_LENGTH}
|
||||
rightIcon={
|
||||
<Text variant="body" color="textSubdued">
|
||||
{`${deviceNameValue.length}/${MAX_LENGTH}`}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
<FormSubmitButton
|
||||
onPress={onSubmit}
|
||||
isVisible={isSubmittable}
|
||||
testID="@device-name/submit-button"
|
||||
>
|
||||
<Translation id="moduleDeviceSettings.changeDeviceName.submitButton" />
|
||||
</FormSubmitButton>
|
||||
</Form>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
@@ -2,18 +2,19 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { SUPPORTS_DEVICE_AUTHENTICITY_CHECK } from '@suite-common/suite-constants';
|
||||
import {
|
||||
selectDeviceLabel,
|
||||
selectDeviceModel,
|
||||
selectDeviceName,
|
||||
selectIsDeviceConnectedViaBluetooth,
|
||||
} from '@suite-common/wallet-core';
|
||||
import { Text, VStack } from '@suite-native/atoms';
|
||||
import { DeviceImage } from '@suite-native/device';
|
||||
import { VStack } from '@suite-native/atoms';
|
||||
import { Translation } from '@suite-native/intl';
|
||||
import { Screen, ScreenHeader } from '@suite-native/navigation';
|
||||
|
||||
import { DeviceAuthenticityCard } from '../components/DeviceAuthenticityCard';
|
||||
import { DeviceBluetoothCard } from '../components/DeviceBluetoothCard';
|
||||
import { DeviceFirmwareCard } from '../components/DeviceFirmwareCard';
|
||||
import { DeviceInfo } from '../components/DeviceInfo';
|
||||
import { DevicePinProtectionCard } from '../components/DevicePinProtectionCard';
|
||||
import { DeviceSettingsSection } from '../components/DeviceSettingsSection';
|
||||
import { WipeDeviceCard } from '../components/WipeDeviceCard';
|
||||
@@ -24,6 +25,7 @@ export const DeviceSettingsModalScreen = () => {
|
||||
|
||||
const deviceModel = useSelector(selectDeviceModel);
|
||||
const deviceName = useSelector(selectDeviceName);
|
||||
const deviceLabel = useSelector(selectDeviceLabel);
|
||||
const isDeviceConnectedViaBluetooth = useSelector(selectIsDeviceConnectedViaBluetooth);
|
||||
|
||||
if (!deviceModel || !deviceName) {
|
||||
@@ -33,10 +35,7 @@ export const DeviceSettingsModalScreen = () => {
|
||||
return (
|
||||
<Screen header={<ScreenHeader closeActionType="close" />}>
|
||||
<VStack spacing="sp40">
|
||||
<VStack marginTop="sp24" spacing="sp24" alignItems="center">
|
||||
<DeviceImage deviceModel={deviceModel} />
|
||||
<Text variant="titleMedium">{deviceName}</Text>
|
||||
</VStack>
|
||||
<DeviceInfo deviceName={deviceLabel || deviceName} deviceModel={deviceModel} />
|
||||
<DeviceSettingsSection
|
||||
title={<Translation id="moduleDeviceSettings.sectionTitles.general" />}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{ "path": "../navigation" },
|
||||
{ "path": "../../packages/connect" },
|
||||
{ "path": "../../packages/device-utils" },
|
||||
{ "path": "../../packages/styles" }
|
||||
{ "path": "../../packages/styles" },
|
||||
{ "path": "../../packages/utils" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import Animated, {
|
||||
FadeInDown,
|
||||
FadeOutDown,
|
||||
SlideInDown,
|
||||
SlideOutDown,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
|
||||
import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core';
|
||||
import { BottomSheet, Button, HStack, InlineAlertBox, Text, VStack } from '@suite-native/atoms';
|
||||
import { BottomSheet, HStack, InlineAlertBox, Text, VStack } from '@suite-native/atoms';
|
||||
import { CryptoAmountFormatter, CryptoToFiatAmountFormatter } from '@suite-native/formatters';
|
||||
import { useFormContext } from '@suite-native/forms';
|
||||
import { FormSubmitButton, useFormContext } from '@suite-native/forms';
|
||||
import { Translation } from '@suite-native/intl';
|
||||
import { SendStackParamList, SendStackRoutes, StackProps } from '@suite-native/navigation';
|
||||
|
||||
@@ -58,13 +51,6 @@ export const CustomFeeBottomSheet = ({ isVisible, onClose }: CustomFeeBottomShee
|
||||
onClose();
|
||||
});
|
||||
|
||||
const animatedButtonContainerStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
height: withTiming(isSubmittable && isVisible ? 50 : 0),
|
||||
}),
|
||||
[isSubmittable, isVisible],
|
||||
);
|
||||
|
||||
if (!symbol) return null;
|
||||
|
||||
return (
|
||||
@@ -108,19 +94,13 @@ export const CustomFeeBottomSheet = ({ isVisible, onClose }: CustomFeeBottomShee
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<Animated.View style={animatedButtonContainerStyle}>
|
||||
{isSubmittable && (
|
||||
<Animated.View entering={SlideInDown} exiting={SlideOutDown}>
|
||||
<Button
|
||||
onPress={handleSetCustomFee}
|
||||
testID="@send/custom-fee-submit-button"
|
||||
>
|
||||
<Translation id="moduleSend.fees.custom.bottomSheet.confirmButton" />
|
||||
</Button>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
<FormSubmitButton
|
||||
onPress={handleSetCustomFee}
|
||||
isVisible={isSubmittable && isVisible}
|
||||
testID="@send/custom-fee-submit-button"
|
||||
>
|
||||
<Translation id="moduleSend.fees.custom.bottomSheet.confirmButton" />
|
||||
</FormSubmitButton>
|
||||
</VStack>
|
||||
</BottomSheet>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
AuthorizeDeviceStackRoutes,
|
||||
DevUtilsStackRoutes,
|
||||
DeviceAuthenticityStackRoutes,
|
||||
DeviceNameStackRoutes,
|
||||
DeviceOnboardingStackRoutes,
|
||||
DevicePinProtectionStackRoutes,
|
||||
DeviceSettingsStackRoutes,
|
||||
@@ -202,6 +203,7 @@ export type DeviceSettingsStackParamList = {
|
||||
[DeviceSettingsStackRoutes.ConfirmFirmwareUpdate]: undefined;
|
||||
[DeviceSettingsStackRoutes.FirmwareInstallation]: undefined;
|
||||
[DeviceSettingsStackRoutes.ContinueOnTrezor]: undefined;
|
||||
[DeviceSettingsStackRoutes.DeviceNameStack]: undefined;
|
||||
[DeviceSettingsStackRoutes.WipeDeviceStack]: NavigatorScreenParams<WipeDeviceStackParamList>;
|
||||
[DeviceSettingsStackRoutes.PinProtection]: undefined;
|
||||
};
|
||||
@@ -219,6 +221,12 @@ export type WipeDeviceStackParamList = {
|
||||
[WipeDeviceStackRoutes.WipeDeviceLoadingScreen]: undefined;
|
||||
};
|
||||
|
||||
export type DeviceNameStackParamList = {
|
||||
[DeviceNameStackRoutes.DeviceName]: undefined;
|
||||
[DeviceNameStackRoutes.ContinueOnTrezor]: undefined;
|
||||
[DeviceNameStackRoutes.DeviceNameLoadingScreen]: undefined;
|
||||
};
|
||||
|
||||
export type DeviceAuthenticityStackParamList = {
|
||||
[DeviceAuthenticityStackRoutes.AuthenticityCheck]: undefined;
|
||||
[DeviceAuthenticityStackRoutes.AuthenticitySuccess]: undefined;
|
||||
|
||||
@@ -79,6 +79,7 @@ export enum DeviceSettingsStackRoutes {
|
||||
FirmwareInstallation = 'FirmwareInstallation',
|
||||
ContinueOnTrezor = 'ContinueOnTrezor',
|
||||
WipeDeviceStack = 'WipeDeviceStack',
|
||||
DeviceNameStack = 'DeviceNameStack',
|
||||
}
|
||||
|
||||
export enum DevicePinProtectionStackRoutes {
|
||||
@@ -99,6 +100,12 @@ export enum WipeDeviceStackRoutes {
|
||||
WipeDeviceLoadingScreen = 'WipeDeviceLoadingScreen',
|
||||
}
|
||||
|
||||
export enum DeviceNameStackRoutes {
|
||||
DeviceName = 'DeviceName',
|
||||
ContinueOnTrezor = 'ContinueOnTrezor',
|
||||
DeviceNameLoadingScreen = 'DeviceNameLoadingScreen',
|
||||
}
|
||||
|
||||
export enum AuthorizeDeviceStackRoutes {
|
||||
ConnectAndUnlockDevice = 'ConnectAndUnlockDevice',
|
||||
ConnectBluetoothDevice = 'ConnectBluetoothDevice',
|
||||
|
||||
@@ -10443,6 +10443,7 @@ __metadata:
|
||||
react: "npm:19.0.0"
|
||||
react-hook-form: "npm:^7.56.3"
|
||||
react-native: "npm:0.79.3"
|
||||
react-native-reanimated: "npm:^3.18.0"
|
||||
yup: "npm:^1.6.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -10844,6 +10845,7 @@ __metadata:
|
||||
"@trezor/connect": "workspace:*"
|
||||
"@trezor/device-utils": "workspace:*"
|
||||
"@trezor/styles": "workspace:*"
|
||||
"@trezor/utils": "workspace:*"
|
||||
react: "npm:19.0.0"
|
||||
react-native: "npm:0.79.3"
|
||||
react-redux: "npm:9.2.0"
|
||||
|
||||
Reference in New Issue
Block a user