This commit is contained in:
Marcos Rodriguez Velez 2024-08-02 13:50:51 -04:00
parent 6f3e02c8a4
commit 30f05d57ac
No known key found for this signature in database
GPG Key ID: 6030B2F48CCE86D7
4 changed files with 104 additions and 63 deletions

View File

@ -9,6 +9,7 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapt
export const MODAL_TYPES = { export const MODAL_TYPES = {
ENTER_PASSWORD: 'ENTER_PASSWORD', ENTER_PASSWORD: 'ENTER_PASSWORD',
CREATE_PASSWORD: 'CREATE_PASSWORD', CREATE_PASSWORD: 'CREATE_PASSWORD',
SUCCESS: 'SUCCESS',
} as const; } as const;
export type ModalType = (typeof MODAL_TYPES)[keyof typeof MODAL_TYPES]; export type ModalType = (typeof MODAL_TYPES)[keyof typeof MODAL_TYPES];
@ -17,7 +18,6 @@ interface PromptPasswordConfirmationModalProps {
modalType: ModalType; modalType: ModalType;
onConfirmationSuccess: (password: string) => Promise<boolean>; onConfirmationSuccess: (password: string) => Promise<boolean>;
onConfirmationFailure: () => void; onConfirmationFailure: () => void;
onDismiss?: () => void;
} }
export interface PromptPasswordConfirmationModalHandle { export interface PromptPasswordConfirmationModalHandle {
@ -26,7 +26,7 @@ export interface PromptPasswordConfirmationModalHandle {
} }
const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationModalHandle, PromptPasswordConfirmationModalProps>( const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationModalHandle, PromptPasswordConfirmationModalProps>(
({ modalType, onConfirmationSuccess, onConfirmationFailure, onDismiss }, ref) => { ({ modalType, onConfirmationSuccess, onConfirmationFailure }, ref) => {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -35,7 +35,10 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
const fadeOutAnimation = useRef(new Animated.Value(1)).current; const fadeOutAnimation = useRef(new Animated.Value(1)).current;
const fadeInAnimation = useRef(new Animated.Value(0)).current; const fadeInAnimation = useRef(new Animated.Value(0)).current;
const scaleAnimation = useRef(new Animated.Value(1)).current; const scaleAnimation = useRef(new Animated.Value(1)).current;
const shakeAnimation = useRef(new Animated.Value(0)).current;
const { colors } = useTheme(); const { colors } = useTheme();
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
modalContent: { modalContent: {
@ -60,6 +63,11 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
present: async () => { present: async () => {
resetState(); resetState();
modalRef.current?.present(); modalRef.current?.present();
if (modalType === MODAL_TYPES.CREATE_PASSWORD) {
passwordInputRef.current?.focus();
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
passwordInputRef.current?.focus();
}
}, },
dismiss: async () => modalRef.current?.dismiss(), dismiss: async () => modalRef.current?.dismiss(),
})); }));
@ -72,6 +80,33 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
fadeOutAnimation.setValue(1); fadeOutAnimation.setValue(1);
fadeInAnimation.setValue(0); fadeInAnimation.setValue(0);
scaleAnimation.setValue(1); scaleAnimation.setValue(1);
shakeAnimation.setValue(0);
};
const handleShakeAnimation = () => {
Animated.sequence([
Animated.timing(shakeAnimation, {
toValue: 10,
duration: 100,
easing: Easing.bounce,
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -10,
duration: 100,
easing: Easing.bounce,
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 0,
duration: 100,
easing: Easing.bounce,
useNativeDriver: true,
}),
]).start(() => {
confirmPasswordInputRef.current?.focus();
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
});
}; };
const handleSuccessAnimation = () => { const handleSuccessAnimation = () => {
@ -119,8 +154,8 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
} }
} else { } else {
setIsLoading(false); setIsLoading(false);
handleShakeAnimation();
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
} }
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) { } else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
success = await onConfirmationSuccess(password); success = await onConfirmationSuccess(password);
@ -129,8 +164,8 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation(); handleSuccessAnimation();
} else { } else {
handleShakeAnimation();
triggerHapticFeedback(HapticFeedbackTypes.NotificationError); triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
} }
} }
}; };
@ -143,6 +178,11 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
useEffect(() => { useEffect(() => {
resetState(); resetState();
if (modalType === MODAL_TYPES.CREATE_PASSWORD) {
passwordInputRef.current?.focus();
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
passwordInputRef.current?.focus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [modalType]); }, [modalType]);
@ -156,7 +196,7 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
<BottomModal <BottomModal
ref={modalRef} ref={modalRef}
showCloseButton={false} showCloseButton={false}
onDismiss={onDismiss} onDismiss={handleCancel}
grabber={false} grabber={false}
sizes={[350]} sizes={[350]}
backgroundColor={colors.modal} backgroundColor={colors.modal}
@ -165,11 +205,12 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
(!isSuccess && ( (!isSuccess && (
<Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}> <Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}>
<View style={styles.feeModalFooter}> <View style={styles.feeModalFooter}>
<SecondButton title={loc._.cancel} onPress={handleCancel} disabled={isLoading} /> <SecondButton testID="CancelButton" title={loc._.cancel} onPress={handleCancel} disabled={isLoading} />
<View style={styles.feeModalFooterSpacing} /> <View style={styles.feeModalFooterSpacing} />
<SecondButton <SecondButton
title={isLoading ? '' : loc._.ok} title={isLoading ? '' : loc._.ok}
onPress={handleSubmit} onPress={handleSubmit}
testID="OKButton"
loading={isLoading} loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)} disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/> />
@ -185,26 +226,32 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
{modalType === MODAL_TYPES.CREATE_PASSWORD ? loc.settings.password_explain : loc._.enter_password} {modalType === MODAL_TYPES.CREATE_PASSWORD ? loc.settings.password_explain : loc._.enter_password}
</Text> </Text>
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
<TextInput <Animated.View style={{ transform: [{ translateX: shakeAnimation }] }}>
testID="PasswordInput"
secureTextEntry
placeholder="Password"
value={password}
onChangeText={setPassword}
style={[styles.input, stylesHook.input]}
autoFocus
onSubmitEditing={handleSubmit} // Handle Enter key as OK
/>
{modalType === MODAL_TYPES.CREATE_PASSWORD && (
<TextInput <TextInput
testID="ConfirmPasswordInput" testID="PasswordInput"
ref={passwordInputRef}
secureTextEntry secureTextEntry
placeholder="Confirm Password" placeholder="Password"
value={confirmPassword} value={password}
onChangeText={setConfirmPassword} onChangeText={setPassword}
style={[styles.input, stylesHook.input]} style={[styles.input, stylesHook.input]}
autoFocus
onSubmitEditing={handleSubmit} // Handle Enter key as OK onSubmitEditing={handleSubmit} // Handle Enter key as OK
/> />
</Animated.View>
{modalType === MODAL_TYPES.CREATE_PASSWORD && (
<Animated.View style={{ transform: [{ translateX: shakeAnimation }] }}>
<TextInput
testID="ConfirmPasswordInput"
ref={confirmPasswordInputRef}
secureTextEntry
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
style={[styles.input, stylesHook.input]}
onSubmitEditing={handleSubmit} // Handle Enter key as OK
/>
</Animated.View>
)} )}
</View> </View>
</Animated.View> </Animated.View>
@ -282,4 +329,4 @@ const styles = StyleSheet.create({
color: 'white', color: 'white',
fontSize: 30, fontSize: 30,
}, },
}); });

View File

@ -17,6 +17,7 @@ type SecondButtonProps = {
title?: string; title?: string;
onPress?: () => void; onPress?: () => void;
loading?: boolean; loading?: boolean;
testID?: string;
}; };
export const SecondButton = forwardRef<TouchableOpacity, SecondButtonProps>((props, ref) => { export const SecondButton = forwardRef<TouchableOpacity, SecondButtonProps>((props, ref) => {
@ -41,6 +42,7 @@ export const SecondButton = forwardRef<TouchableOpacity, SecondButtonProps>((pro
<TouchableOpacity <TouchableOpacity
disabled={props.disabled || props.loading} disabled={props.disabled || props.loading}
accessibilityRole="button" accessibilityRole="button"
testID={props.testID}
style={[styles.button, { backgroundColor }]} style={[styles.button, { backgroundColor }]}
{...props} {...props}
ref={ref} ref={ref}

View File

@ -19,8 +19,6 @@ enum ActionType {
SetDeviceBiometricCapable = 'SET_DEVICE_BIOMETRIC_CAPABLE', SetDeviceBiometricCapable = 'SET_DEVICE_BIOMETRIC_CAPABLE',
SetCurrentLoadingSwitch = 'SET_CURRENT_LOADING_SWITCH', SetCurrentLoadingSwitch = 'SET_CURRENT_LOADING_SWITCH',
SetModalType = 'SET_MODAL_TYPE', SetModalType = 'SET_MODAL_TYPE',
SetIsSuccess = 'SET_IS_SUCCESS',
ResetState = 'RESET_STATE',
} }
interface Action { interface Action {
@ -34,7 +32,6 @@ interface State {
deviceBiometricCapable: boolean; deviceBiometricCapable: boolean;
currentLoadingSwitch: string | null; currentLoadingSwitch: string | null;
modalType: keyof typeof MODAL_TYPES; modalType: keyof typeof MODAL_TYPES;
isSuccess: boolean;
} }
const initialState: State = { const initialState: State = {
@ -43,7 +40,6 @@ const initialState: State = {
deviceBiometricCapable: false, deviceBiometricCapable: false,
currentLoadingSwitch: null, currentLoadingSwitch: null,
modalType: MODAL_TYPES.ENTER_PASSWORD, modalType: MODAL_TYPES.ENTER_PASSWORD,
isSuccess: false,
}; };
const reducer = (state: State, action: Action): State => { const reducer = (state: State, action: Action): State => {
@ -58,10 +54,6 @@ const reducer = (state: State, action: Action): State => {
return { ...state, currentLoadingSwitch: action.payload }; return { ...state, currentLoadingSwitch: action.payload };
case ActionType.SetModalType: case ActionType.SetModalType:
return { ...state, modalType: action.payload }; return { ...state, modalType: action.payload };
case ActionType.SetIsSuccess:
return { ...state, isSuccess: action.payload };
case ActionType.ResetState:
return initialState;
default: default:
return state; return state;
} }
@ -97,7 +89,6 @@ const EncryptStorage = () => {
}, [initializeState]); }, [initializeState]);
const handleDecryptStorage = async () => { const handleDecryptStorage = async () => {
dispatch({ type: ActionType.SetCurrentLoadingSwitch, payload: 'decrypt' });
dispatch({ type: ActionType.SetModalType, payload: MODAL_TYPES.ENTER_PASSWORD }); dispatch({ type: ActionType.SetModalType, payload: MODAL_TYPES.ENTER_PASSWORD });
promptRef.current?.present(); promptRef.current?.present();
}; };
@ -158,10 +149,6 @@ const EncryptStorage = () => {
); );
}; };
const onModalDismiss = () => {
initializeState(); // Reinitialize state on modal dismiss to refresh the UI
};
return ( return (
<ScrollView <ScrollView
contentContainerStyle={[styles.root, styleHooks.root]} contentContainerStyle={[styles.root, styleHooks.root]}
@ -222,6 +209,7 @@ const EncryptStorage = () => {
try { try {
await encryptStorage(password); await encryptStorage(password);
await saveToDisk(); await saveToDisk();
dispatch({ type: ActionType.SetModalType, payload: MODAL_TYPES.SUCCESS });
success = true; success = true;
} catch (error) { } catch (error) {
presentAlert({ title: loc.errors.error, message: (error as Error).message }); presentAlert({ title: loc.errors.error, message: (error as Error).message });
@ -233,17 +221,18 @@ const EncryptStorage = () => {
await saveToDisk(); await saveToDisk();
success = true; success = true;
} catch (error) { } catch (error) {
presentAlert({ message: loc._.bad_password });
success = false; success = false;
} }
} }
dispatch({ type: ActionType.SetLoading, payload: false });
dispatch({ type: ActionType.SetCurrentLoadingSwitch, payload: null });
initializeState();
return success; return success;
}} }}
onConfirmationFailure={() => { onConfirmationFailure={() => {
dispatch({ type: ActionType.SetLoading, payload: false }); dispatch({ type: ActionType.SetLoading, payload: false });
dispatch({ type: ActionType.SetCurrentLoadingSwitch, payload: null }); dispatch({ type: ActionType.SetCurrentLoadingSwitch, payload: null });
}} }}
onDismiss={onModalDismiss}
/> />
</ScrollView> </ScrollView>
); );

View File

@ -12,6 +12,7 @@ import {
sup, sup,
yo, yo,
} from './helperz'; } from './helperz';
import { element } from 'detox';
/** /**
* this testsuite is for test cases that require no wallets to be present * this testsuite is for test cases that require no wallets to be present
@ -248,19 +249,19 @@ describe('BlueWallet UI Tests - no wallets', () => {
// lets encrypt the storage. // lets encrypt the storage.
// first, trying to mistype second password: // first, trying to mistype second password:
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol. lets tap it await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol. lets tap it
await element(by.type('android.widget.EditText')).typeText('08902'); await element(by.id('PasswordInput')).typeText('08902');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).tapReturnKey();
await element(by.type('android.widget.EditText')).typeText('666'); await element(by.id('ConfirmPasswordInput')).typeText('666');
await element(by.text('OK')).tap(); await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await expect(element(by.text('Passwords do not match.'))).toBeVisible(); await element(by.id('OKButton')).tap();
await element(by.text('OK')).tap();
// now, lets put correct passwords and encrypt the storage // now, lets put correct passwords and encrypt the storage
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol await element(by.id('PasswordInput')).clearText();
await element(by.type('android.widget.EditText')).typeText('qqq'); await element(by.id('PasswordInput')).typeText('qqq');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).tapReturnKey();
await element(by.type('android.widget.EditText')).typeText('qqq'); await element(by.id('ConfirmPasswordInput')).typeText('qqq');
await element(by.text('OK')).tap(); await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
// relaunch app // relaunch app
await device.launchApp({ newInstance: true }); await device.launchApp({ newInstance: true });
@ -391,12 +392,16 @@ describe('BlueWallet UI Tests - no wallets', () => {
// lets encrypt the storage. // lets encrypt the storage.
// lets put correct passwords and encrypt the storage // lets put correct passwords and encrypt the storage
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.type('android.widget.EditText')).typeText('pass');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).clearText();
await element(by.type('android.widget.EditText')).typeText('pass'); await element(by.id('PasswordInput')).typeText('pass');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('pass');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
await element(by.id('PlausibleDeniabilityButton')).tap(); await element(by.id('PlausibleDeniabilityButton')).tap();
// trying to enable plausible denability // trying to enable plausible denability
await element(by.id('CreateFakeStorageButton')).tap(); await element(by.id('CreateFakeStorageButton')).tap();
await element(by.type('android.widget.EditText')).typeText('fake'); await element(by.type('android.widget.EditText')).typeText('fake');
@ -418,8 +423,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
.withTimeout(33000); .withTimeout(33000);
// //
await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible(); await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible();
await element(by.type('android.widget.EditText')).typeText('pass'); await element(by.id('PasswordInput')).typeText('pass');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
await yo('WalletsList'); await yo('WalletsList');
// previously created wallet IN MAIN STORAGE should be visible // previously created wallet IN MAIN STORAGE should be visible
@ -432,16 +438,13 @@ describe('BlueWallet UI Tests - no wallets', () => {
// putting FAKE storage password. should not succeed // putting FAKE storage password. should not succeed
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('fake'); await element(by.id('PasswordInput')).typeText('fake');
await element(by.text('OK')).tap(); await element(by.id('PasswordInput')).tapReturnKey();
await expect(element(by.text('Incorrect password. Please try again.'))).toBeVisible(); await element(by.id('OKButton')).tap();
await element(by.text('OK')).tap();
// correct password // correct password
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol await element(by.ic('PasswordInput')).typeText('pass');
await element(by.text('OK')).tap(); await element(by.ic('PasswordInput')).tapReturnKey();
await element(by.type('android.widget.EditText')).typeText('pass'); await element(by.text('OKButton')).tap();
await element(by.text('OK')).tap();
// relaunch app // relaunch app
await device.launchApp({ newInstance: true }); await device.launchApp({ newInstance: true });