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 = {
ENTER_PASSWORD: 'ENTER_PASSWORD',
CREATE_PASSWORD: 'CREATE_PASSWORD',
SUCCESS: 'SUCCESS',
} as const;
export type ModalType = (typeof MODAL_TYPES)[keyof typeof MODAL_TYPES];
@ -17,7 +18,6 @@ interface PromptPasswordConfirmationModalProps {
modalType: ModalType;
onConfirmationSuccess: (password: string) => Promise<boolean>;
onConfirmationFailure: () => void;
onDismiss?: () => void;
}
export interface PromptPasswordConfirmationModalHandle {
@ -26,7 +26,7 @@ export interface PromptPasswordConfirmationModalHandle {
}
const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationModalHandle, PromptPasswordConfirmationModalProps>(
({ modalType, onConfirmationSuccess, onConfirmationFailure, onDismiss }, ref) => {
({ modalType, onConfirmationSuccess, onConfirmationFailure }, ref) => {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
@ -35,7 +35,10 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
const fadeOutAnimation = useRef(new Animated.Value(1)).current;
const fadeInAnimation = useRef(new Animated.Value(0)).current;
const scaleAnimation = useRef(new Animated.Value(1)).current;
const shakeAnimation = useRef(new Animated.Value(0)).current;
const { colors } = useTheme();
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const stylesHook = StyleSheet.create({
modalContent: {
@ -60,6 +63,11 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
present: async () => {
resetState();
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(),
}));
@ -72,6 +80,33 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
fadeOutAnimation.setValue(1);
fadeInAnimation.setValue(0);
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 = () => {
@ -119,8 +154,8 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
}
} else {
setIsLoading(false);
handleShakeAnimation();
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
}
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
success = await onConfirmationSuccess(password);
@ -129,8 +164,8 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
handleShakeAnimation();
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
}
}
};
@ -143,6 +178,11 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
useEffect(() => {
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
}, [modalType]);
@ -156,7 +196,7 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
<BottomModal
ref={modalRef}
showCloseButton={false}
onDismiss={onDismiss}
onDismiss={handleCancel}
grabber={false}
sizes={[350]}
backgroundColor={colors.modal}
@ -165,11 +205,12 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
(!isSuccess && (
<Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}>
<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} />
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
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}
</Text>
<View style={styles.inputContainer}>
<TextInput
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 && (
<Animated.View style={{ transform: [{ translateX: shakeAnimation }] }}>
<TextInput
testID="ConfirmPasswordInput"
testID="PasswordInput"
ref={passwordInputRef}
secureTextEntry
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Password"
value={password}
onChangeText={setPassword}
style={[styles.input, stylesHook.input]}
autoFocus
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>
</Animated.View>
@ -282,4 +329,4 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: 30,
},
});
});

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
sup,
yo,
} from './helperz';
import { element } from 'detox';
/**
* 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.
// 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.EditText')).typeText('08902');
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('666');
await element(by.text('OK')).tap();
await expect(element(by.text('Passwords do not match.'))).toBeVisible();
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).typeText('08902');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('666');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
// now, 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.EditText')).typeText('qqq');
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('qqq');
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('qqq');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('qqq');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
// relaunch app
await device.launchApp({ newInstance: true });
@ -391,12 +392,16 @@ describe('BlueWallet UI Tests - no wallets', () => {
// lets 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.EditText')).typeText('pass');
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('pass');
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('pass');
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();
// trying to enable plausible denability
await element(by.id('CreateFakeStorageButton')).tap();
await element(by.type('android.widget.EditText')).typeText('fake');
@ -418,8 +423,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
.withTimeout(33000);
//
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.text('OK')).tap();
await element(by.id('PasswordInput')).typeText('pass');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
await yo('WalletsList');
// 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
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('fake');
await element(by.text('OK')).tap();
await expect(element(by.text('Incorrect password. Please try again.'))).toBeVisible();
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).typeText('fake');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
// correct password
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.text('OK')).tap();
await element(by.type('android.widget.EditText')).typeText('pass');
await element(by.text('OK')).tap();
await element(by.ic('PasswordInput')).typeText('pass');
await element(by.ic('PasswordInput')).tapReturnKey();
await element(by.text('OKButton')).tap();
// relaunch app
await device.launchApp({ newInstance: true });