Skip to content

Commit

Permalink
Feat/empty mobile passphrase (#12193)
Browse files Browse the repository at this point in the history
* feat(suite-native): empty passphrase wallet UI

* fixup! feat(suite-native): empty passphrase wallet UI

* fixup! feat(suite-native): empty passphrase wallet UI
  • Loading branch information
juriczech authored May 6, 2024
1 parent d18561d commit 26c4923
Show file tree
Hide file tree
Showing 23 changed files with 476 additions and 39 deletions.
5 changes: 5 additions & 0 deletions suite-common/icons/assets/icons/eyeSlashLight.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions suite-common/icons/assets/icons/pencilUnderscored.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions suite-common/icons/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const icons = {
externalLink: require('../assets/icons/externalLink.svg'),
eye: require('../assets/icons/eye.svg'),
eyeSlash: require('../assets/icons/eyeSlash.svg'),
eyeSlashLight: require('../assets/icons/eyeSlashLight.svg'),
eyeglasses: require('../assets/icons/eyeglasses.svg'),
face: require('../assets/icons/face.svg'),
faceId: require('../assets/icons/faceId.svg'),
Expand Down Expand Up @@ -68,6 +69,7 @@ export const icons = {
password: require('../assets/icons/password.svg'),
pdf: require('../assets/icons/pdf.svg'),
pencil: require('../assets/icons/pencil.svg'),
pencilUnderscored: require('../assets/icons/pencilUnderscored.svg'),
placeholder: require('../assets/icons/placeholder.svg'),
plus: require('../assets/icons/plus.svg'),
plusCircle: require('../assets/icons/plusCircle.svg'),
Expand Down
6 changes: 6 additions & 0 deletions suite-common/wallet-core/src/device/deviceReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,3 +860,9 @@ export const selectPhysicalDevicesGrouppedById = memoize((state: DeviceRootState

return deviceUtils.getDeviceInstancesGroupedByDeviceId(devices);
});

export const selectDeviceState = (state: DeviceRootState) => {
const device = selectDevice(state);

return device?.state ?? null;
};
2 changes: 1 addition & 1 deletion suite-native/biometrics/src/useBiometricsSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback } from 'react';

import { useAlert } from '@suite-native/alerts/src';
import { useAlert } from '@suite-native/alerts';
import { analytics, EventType } from '@suite-native/analytics';

import {
Expand Down
20 changes: 16 additions & 4 deletions suite-native/device/src/hooks/useHandleDeviceRequestsPassphrase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';

import { useNavigation } from '@react-navigation/native';

Expand All @@ -10,6 +11,7 @@ import {
RootStackRoutes,
StackToStackCompositeNavigationProps,
} from '@suite-native/navigation';
import { selectDeviceState } from '@suite-common/wallet-core';

type NavigationProp = StackToStackCompositeNavigationProps<
PassphraseStackParamList,
Expand All @@ -20,11 +22,21 @@ type NavigationProp = StackToStackCompositeNavigationProps<
export const useHandleDeviceRequestsPassphrase = () => {
const navigation = useNavigation<NavigationProp>();

const deviceState = useSelector(selectDeviceState);

const handleNavigateToPassphraseForm = useCallback(() => {
navigation.navigate(RootStackRoutes.PassphraseStack, {
screen: PassphraseStackRoutes.PassphraseForm,
});
}, [navigation]);
if (deviceState) {
// If device already has a state, it means it's already been authorized with passphrase
// and we are trying to verify that user has correct passphrase that has been used to authorize this wallet
navigation.navigate(RootStackRoutes.PassphraseStack, {
screen: PassphraseStackRoutes.PassphraseVerifyEmptyWallet,
});
} else {
navigation.navigate(RootStackRoutes.PassphraseStack, {
screen: PassphraseStackRoutes.PassphraseForm,
});
}
}, [deviceState, navigation]);

useEffect(() => {
TrezorConnect.on('ui-request_passphrase', handleNavigateToPassphraseForm);
Expand Down
36 changes: 35 additions & 1 deletion suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,8 @@ export const en = {
},
form: {
enterWallet: 'Enter passphrase',
inputLabel: 'Enter your passphrase',
createWalletInputLabel: 'Enter your passphrase',
verifyPassphraseInputLabel: 'Re-enter your passphrase',
},
confirmOnDevice: {
title: 'Confirm passphrase\non your Trezor.',
Expand All @@ -663,6 +664,39 @@ export const en = {
secondaryButton: 'Continue opening',
},
},
emptyPassphraseWallet: {
title: 'This passphrase wallet is empty',
confirmCard: {
description: 'Opening unused and knowingly empty passphrase wallet?',
button: 'Yes, open unused wallet',
},
expectingPassphraseWallet: {
title: 'Expecting passphrase wallet with funds?',
description: "You might've made a typo. Try entering your passphrase again.",
button: 'Try again',
},
confirmEmptyWalletSheet: {
title: 'What to do with new passphrase?',
list: {
backup: 'Backup your passphrase on a piece of paper and always keep it offline (no photos, USB, internet).',
store: 'Store it elsewhere than your recovery seed and Trezor.',
neverShare: 'Never share it with anyone, not even with Trezor support.',
},
button: 'Got it, continue',
alertTitle: 'No one can recover your passphrase, not even Trezor support',
},
verifyEmptyWallet: {
title: 'Confirm empty passphrase',
description: 'Carefully re-enter your passphrase to confirm it.',
alertTitle:
'Create an offline backup of your passphrase. It is irrecoverable, even by Trezor support.',
passphraseMismatchAlert: {
title: 'Passphrase mismatch',
description: "Passphrase doesn't match",
buttonTitle: 'Start over',
},
},
},
},
};

Expand Down
1 change: 1 addition & 0 deletions suite-native/module-passphrase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@suite-common/validators": "workspace:*",
"@suite-common/wallet-constants": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-native/alerts": "workspace:*",
"@suite-native/atoms": "workspace:*",
"@suite-native/forms": "workspace:*",
"@suite-native/intl": "workspace:*",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions suite-native/module-passphrase/src/components/EmptyWalletInfoSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useNavigation } from '@react-navigation/native';

import { Icon, IconName } from '@suite-common/icons';
import { TxKeyPath } from '@suite-native/intl';
import {
AlertBox,
BottomSheet,
Box,
Button,
HStack,
Pictogram,
Text,
VStack,
} from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import {
PassphraseStackParamList,
PassphraseStackRoutes,
RootStackParamList,
StackToStackCompositeNavigationProps,
} from '@suite-native/navigation';

type EmptyWalletInfoSheetProps = {
onClose: () => void;
isVisible: boolean;
};

const buttonWrapperStyle = prepareNativeStyle(() => ({
width: '100%',
}));

const listItemStyle = prepareNativeStyle(() => ({
width: '90%',
}));

const ListItem = ({
iconName,
translationKey,
}: {
iconName: IconName;
translationKey: TxKeyPath;
}) => {
const { applyStyle } = useNativeStyles();

return (
<HStack spacing={20}>
<Icon name={iconName} />
<Text style={applyStyle(listItemStyle)}>
<Translation id={translationKey} />
</Text>
</HStack>
);
};

type NavigationProp = StackToStackCompositeNavigationProps<
PassphraseStackParamList,
PassphraseStackRoutes.PassphraseEmptyWallet,
RootStackParamList
>;

export const EmptyWalletInfoSheet = ({ onClose, isVisible }: EmptyWalletInfoSheetProps) => {
const navigation = useNavigation<NavigationProp>();

const { applyStyle } = useNativeStyles();

const handleOpenEmptyWallet = () => {
navigation.navigate(PassphraseStackRoutes.PassphraseVerifyEmptyWallet);
onClose();
};

return (
<BottomSheet isVisible={isVisible} onClose={onClose} isCloseDisplayed={false}>
<VStack alignItems="center" spacing="large" padding="medium">
<Pictogram variant="yellow" icon="warningTriangleLight" />
<VStack spacing="medium">
<Text variant="titleSmall">
<Translation id="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.title" />
</Text>
<VStack spacing={12}>
<ListItem
iconName="pencilUnderscored"
translationKey="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.list.backup"
/>
<ListItem
iconName="copy"
translationKey="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.list.store"
/>
<ListItem
iconName="eyeSlashLight"
translationKey="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.list.neverShare"
/>
</VStack>
<AlertBox
variant="warning"
title={
<Translation id="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.alertTitle" />
}
/>
</VStack>
<Box style={applyStyle(buttonWrapperStyle)}>
<Button onPress={handleOpenEmptyWallet}>
<Translation id="modulePassphrase.emptyPassphraseWallet.confirmEmptyWalletSheet.button" />
</Button>
</Box>
</VStack>
</BottomSheet>
);
};
21 changes: 11 additions & 10 deletions suite-native/module-passphrase/src/components/PassphraseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import {
} from '@suite-common/validators';
import { Button, VStack } from '@suite-native/atoms';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { Translation, useTranslate } from '@suite-native/intl';
import { Translation } from '@suite-native/intl';
import {
deviceActions,
onPassphraseSubmit,
selectDevice,
selectDeviceButtonRequestsCodes,
selectDeviceState,
} from '@suite-common/wallet-core';
import {
PassphraseStackParamList,
Expand All @@ -27,7 +28,8 @@ import {
} from '@suite-native/navigation';

type PassphraseFormProps = {
onFocus: () => void;
onFocus?: () => void;
inputLabel: string;
};

const formStyle = prepareNativeStyle(utils => ({
Expand All @@ -41,15 +43,14 @@ type NavigationProp = StackToStackCompositeNavigationProps<
RootStackParamList
>;

export const PassphraseForm = ({ onFocus }: PassphraseFormProps) => {
export const PassphraseForm = ({ inputLabel, onFocus }: PassphraseFormProps) => {
const dispatch = useDispatch();

const { applyStyle } = useNativeStyles();

const { translate } = useTranslate();

const device = useSelector(selectDevice);
const buttonRequestCodes = useSelector(selectDeviceButtonRequestsCodes);
const deviceState = useSelector(selectDeviceState);

const navigation = useNavigation<NavigationProp>();

Expand All @@ -60,14 +61,14 @@ export const PassphraseForm = ({ onFocus }: PassphraseFormProps) => {
},
});

const { handleSubmit, watch } = form;

useEffect(() => {
if (buttonRequestCodes.includes('ButtonRequest_Other')) {
navigation.navigate(PassphraseStackRoutes.PassphraseConfirmOnDevice);
navigation.navigate(PassphraseStackRoutes.PassphraseConfirmOnTrezor);
dispatch(deviceActions.removeButtonRequests({ device }));
}
}, [buttonRequestCodes, device, dispatch, navigation]);

const { handleSubmit, watch } = form;
}, [buttonRequestCodes, device, deviceState, dispatch, navigation]);

const handleCreateHiddenWallet = handleSubmit(({ passphrase }) => {
dispatch(deviceActions.removeButtonRequests({ device }));
Expand All @@ -80,7 +81,7 @@ export const PassphraseForm = ({ onFocus }: PassphraseFormProps) => {
<Form form={form}>
<VStack spacing="medium" padding="medium" style={applyStyle(formStyle)}>
<TextInputField
label={translate('modulePassphrase.form.inputLabel')}
label={inputLabel}
name="passphrase"
maxLength={formInputsMaxLength.passphrase}
accessibilityLabel="passphrase input"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const PassphraseScreenHeader = () => {
};

const handlePress = () => {
if (route.name === PassphraseStackRoutes.PassphraseConfirmOnDevice) {
if (route.name === PassphraseStackRoutes.PassphraseConfirmOnTrezor) {
setShouldShowWarningBottomSheet(true);
} else {
handleClose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {

import { PassphraseFormScreen } from '../screens/PassphraseFormScreen';
import { PassphraseLoadingScreen } from '../screens/PassphraseLoadingScreen';
import { PassphraseConfirmOnDeviceScreen } from '../screens/PassphraseConfirmOnDeviceScreen';
import { PassphraseConfirmOnTrezorScreen } from '../screens/PassphraseConfirmOnTrezorScreen';
import { PassphraseEmptyWalletScreen } from '../screens/PassphraseEmptyWalletScreen';
import { PassphraseVerifyEmptyWalletScreen } from '../screens/PassphraseVerifyEmptyWalletScreen';

export const PassphraseStack = createNativeStackNavigator<PassphraseStackParamList>();

Expand All @@ -20,13 +22,21 @@ export const PassphraseStackNavigator = () => {
component={PassphraseFormScreen}
/>
<PassphraseStack.Screen
name={PassphraseStackRoutes.PassphraseConfirmOnDevice}
component={PassphraseConfirmOnDeviceScreen}
name={PassphraseStackRoutes.PassphraseConfirmOnTrezor}
component={PassphraseConfirmOnTrezorScreen}
/>
<PassphraseStack.Screen
name={PassphraseStackRoutes.PassphraseLoading}
component={PassphraseLoadingScreen}
/>
<PassphraseStack.Screen
name={PassphraseStackRoutes.PassphraseEmptyWallet}
component={PassphraseEmptyWalletScreen}
/>
<PassphraseStack.Screen
name={PassphraseStackRoutes.PassphraseVerifyEmptyWallet}
component={PassphraseVerifyEmptyWalletScreen}
/>
</PassphraseStack.Navigator>
);
};
27 changes: 27 additions & 0 deletions suite-native/module-passphrase/src/passphraseThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,30 @@ export const cancelPassphraseAndSelectStandardDeviceThunk = createThunk(
dispatch(deviceActions.forgetDevice(device));
},
);

export const verifyPassphraseOnEmptyWalletThunk = createThunk(
`${PASSPHRASE_MODULE_PREFIX}/verifyPassphraseOnEmptyWallet`,
async (_, { getState, rejectWithValue, fulfillWithValue }) => {
const device = selectDevice(getState());

if (!device) return;

const response = await TrezorConnect.getDeviceState({
device: {
path: device.path,
instance: device.instance,
// Even though we have device state available, we intentionally send undefined so that connect requests passphrase
// When we submit passphrase, we can then compare both device states (previous and current - they're derived from passphrase)
// to see if they match.
state: undefined,
},
keepSession: false,
});

if (response.success && response.payload.state !== device.state) {
return rejectWithValue(false);
}

return fulfillWithValue(true);
},
);
Loading

0 comments on commit 26c4923

Please sign in to comment.