BlueWallet/components/PromptPasswordConfirmationModal.tsx

444 lines
15 KiB
TypeScript
Raw Normal View History

2024-08-02 19:08:14 +02:00
import React, { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
2024-08-13 18:38:38 +02:00
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager } from 'react-native';
2024-08-02 19:08:14 +02:00
import BottomModal, { BottomModalHandle } from './BottomModal';
2024-08-03 02:51:22 +02:00
import { useTheme } from '../components/themes';
2024-08-02 19:08:14 +02:00
import loc from '../loc';
import { SecondButton } from './SecondButton';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
2024-08-13 18:38:38 +02:00
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
2024-08-02 19:08:14 +02:00
export const MODAL_TYPES = {
ENTER_PASSWORD: 'ENTER_PASSWORD',
CREATE_PASSWORD: 'CREATE_PASSWORD',
2024-08-11 00:54:23 +02:00
CREATE_FAKE_STORAGE: 'CREATE_FAKE_STORAGE',
2024-08-02 19:50:51 +02:00
SUCCESS: 'SUCCESS',
2024-08-02 19:08:14 +02:00
} as const;
export type ModalType = (typeof MODAL_TYPES)[keyof typeof MODAL_TYPES];
interface PromptPasswordConfirmationModalProps {
modalType: ModalType;
onConfirmationSuccess: (password: string) => Promise<boolean>;
onConfirmationFailure: () => void;
}
export interface PromptPasswordConfirmationModalHandle {
present: () => Promise<void>;
dismiss: () => Promise<void>;
}
const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationModalHandle, PromptPasswordConfirmationModalProps>(
2024-08-05 00:08:01 +02:00
({ modalType, onConfirmationSuccess, onConfirmationFailure }, ref) => {
2024-08-02 19:08:14 +02:00
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [showExplanation, setShowExplanation] = useState(false); // State to toggle between explanation and password input for CREATE_PASSWORD and CREATE_FAKE_STORAGE
2024-08-02 19:08:14 +02:00
const modalRef = useRef<BottomModalHandle>(null);
const fadeOutAnimation = useRef(new Animated.Value(1)).current;
const fadeInAnimation = useRef(new Animated.Value(0)).current;
const scaleAnimation = useRef(new Animated.Value(1)).current;
2024-08-02 19:50:51 +02:00
const shakeAnimation = useRef(new Animated.Value(0)).current;
const explanationOpacity = useRef(new Animated.Value(1)).current; // New animated value for opacity
2024-08-02 19:08:14 +02:00
const { colors } = useTheme();
2024-08-02 19:50:51 +02:00
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
2024-08-02 19:08:14 +02:00
const stylesHook = StyleSheet.create({
modalContent: {
backgroundColor: colors.elevated,
width: '100%',
},
input: {
backgroundColor: colors.inputBackgroundColor,
borderColor: colors.formBorder,
color: colors.foregroundColor,
width: '100%',
},
feeModalCustomText: {
color: colors.buttonAlternativeTextColor,
},
feeModalLabel: {
color: colors.successColor,
},
});
useImperativeHandle(ref, () => ({
present: async () => {
resetState();
modalRef.current?.present();
2024-08-11 00:54:23 +02:00
if (modalType === MODAL_TYPES.CREATE_PASSWORD || (modalType === MODAL_TYPES.CREATE_FAKE_STORAGE && !showExplanation)) {
2024-08-02 19:50:51 +02:00
passwordInputRef.current?.focus();
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
passwordInputRef.current?.focus();
}
2024-08-02 19:08:14 +02:00
},
2024-08-03 02:51:22 +02:00
dismiss: async () => {
await modalRef.current?.dismiss();
2024-08-05 00:08:01 +02:00
resetState();
2024-08-03 02:51:22 +02:00
},
2024-08-02 19:08:14 +02:00
}));
const resetState = () => {
setPassword('');
setConfirmPassword('');
setIsSuccess(false);
setIsLoading(false);
fadeOutAnimation.setValue(1);
fadeInAnimation.setValue(0);
scaleAnimation.setValue(1);
2024-08-02 19:50:51 +02:00
shakeAnimation.setValue(0);
2024-08-05 00:08:01 +02:00
explanationOpacity.setValue(1);
2024-08-13 17:33:21 +02:00
setShowExplanation(modalType === MODAL_TYPES.CREATE_PASSWORD);
2024-08-02 19:50:51 +02:00
};
2024-08-05 00:08:01 +02:00
useEffect(() => {
resetState();
2024-08-11 00:54:23 +02:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2024-08-05 00:08:01 +02:00
}, [modalType]);
2024-08-02 19:50:51 +02:00
const handleShakeAnimation = () => {
Animated.sequence([
Animated.timing(shakeAnimation, {
toValue: 10,
duration: 100,
2024-08-05 00:08:01 +02:00
easing: Easing.inOut(Easing.ease),
2024-08-02 19:50:51 +02:00
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -10,
duration: 100,
2024-08-05 00:08:01 +02:00
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 5,
duration: 100,
2024-08-05 00:08:01 +02:00
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -5,
duration: 100,
2024-08-05 00:08:01 +02:00
easing: Easing.inOut(Easing.ease),
2024-08-02 19:50:51 +02:00
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 0,
duration: 100,
2024-08-05 00:08:01 +02:00
easing: Easing.inOut(Easing.ease),
2024-08-02 19:50:51 +02:00
useNativeDriver: true,
}),
]).start(() => {
confirmPasswordInputRef.current?.focus();
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
});
2024-08-02 19:08:14 +02:00
};
const handleSuccessAnimation = () => {
2024-08-13 18:38:38 +02:00
// Step 1: Cross-fade current content out and success content in
Animated.timing(fadeOutAnimation, {
toValue: 0, // Fade out current content
duration: 300,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}).start(() => {
2024-08-02 19:08:14 +02:00
setIsSuccess(true);
2024-08-13 18:38:38 +02:00
2024-08-02 19:08:14 +02:00
Animated.timing(fadeInAnimation, {
2024-08-13 18:38:38 +02:00
toValue: 1, // Fade in success content
duration: 300,
2024-08-02 19:08:14 +02:00
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}).start(() => {
2024-08-13 18:38:38 +02:00
// Step 2: Perform any additional animations like scaling if necessary
Animated.timing(scaleAnimation, {
toValue: 1.1,
duration: 300,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}).start(() => {
Animated.timing(scaleAnimation, {
toValue: 1, // Return scale to normal size
duration: 300,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}).start(() => {
// Optional delay before dismissing the modal
setTimeout(async () => {
await modalRef.current?.dismiss();
}, 1000);
});
});
2024-08-02 19:08:14 +02:00
});
});
};
const handleSubmit = async () => {
2024-08-11 00:54:23 +02:00
Keyboard.dismiss();
2024-08-02 19:08:14 +02:00
setIsLoading(true);
let success = false;
2024-08-13 18:38:38 +02:00
try {
if (modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) {
if (password === confirmPassword && password) {
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
}
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
}
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
2024-08-02 19:08:14 +02:00
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
2024-08-13 18:38:38 +02:00
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
2024-08-02 19:08:14 +02:00
onConfirmationFailure();
}
}
2024-08-13 18:38:38 +02:00
} finally {
setIsLoading(false); // Ensure loading state is reset
2024-08-02 19:08:14 +02:00
if (success) {
2024-08-13 18:38:38 +02:00
// Ensure shake animation is reset before starting the success animation
shakeAnimation.setValue(0);
2024-08-02 19:08:14 +02:00
}
}
};
2024-08-05 00:08:01 +02:00
const handleTransitionToCreatePassword = () => {
Animated.timing(explanationOpacity, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowExplanation(false);
explanationOpacity.setValue(1); // Reset opacity for when transitioning back
passwordInputRef.current?.focus();
});
};
2024-08-02 19:08:14 +02:00
const handleCancel = async () => {
onConfirmationFailure();
await modalRef.current?.dismiss();
};
const animatedViewStyle: Animated.WithAnimatedObject<ViewStyle> = {
opacity: fadeOutAnimation,
transform: [{ scale: scaleAnimation }],
width: '100%',
};
const onModalDismiss = () => {
resetState();
onConfirmationFailure();
};
2024-08-02 19:08:14 +02:00
return (
<BottomModal
ref={modalRef}
onDismiss={onModalDismiss}
2024-08-02 19:08:14 +02:00
grabber={false}
2024-08-24 19:35:13 +02:00
onCloseModalPressed={handleCancel}
2024-08-02 19:08:14 +02:00
backgroundColor={colors.modal}
footer={
2024-08-05 00:08:01 +02:00
!isSuccess ? (
2024-08-24 19:35:13 +02:00
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
<Animated.View style={[{ opacity: explanationOpacity }, styles.feeModalFooterSpacing]}>
<SecondButton
title={loc.settings.i_understand}
onPress={handleTransitionToCreatePassword}
disabled={isLoading}
testID="IUnderstandButton"
/>
</Animated.View>
) : (
2024-08-05 00:08:01 +02:00
<Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}>
<View style={styles.feeModalFooter}>
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
2024-08-13 17:33:21 +02:00
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
2024-08-05 00:08:01 +02:00
/>
</View>
</Animated.View>
)
) : null
2024-08-02 19:08:14 +02:00
}
>
2024-08-05 00:08:01 +02:00
{!isSuccess && (
2024-08-24 19:35:13 +02:00
<Animated.View style={[animatedViewStyle, styles.minHeight]}>
2024-08-13 17:33:21 +02:00
{modalType === MODAL_TYPES.CREATE_PASSWORD && showExplanation && (
2024-08-05 00:08:01 +02:00
<Animated.View style={{ opacity: explanationOpacity }}>
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>{loc.settings.encrypt_storage_explanation_headline}</Text>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
{loc.settings.encrypt_storage_explanation_description_line1}
2024-08-05 00:08:01 +02:00
</Text>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
{loc.settings.encrypt_storage_explanation_description_line2}
2024-08-05 00:08:01 +02:00
</Text>
<View style={styles.feeModalFooter} />
2024-08-02 19:50:51 +02:00
</Animated.View>
2024-08-05 00:08:01 +02:00
)}
2024-08-11 00:54:23 +02:00
{(modalType === MODAL_TYPES.ENTER_PASSWORD ||
((modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) && !showExplanation)) && (
2024-08-05 00:08:01 +02:00
<>
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>
{modalType === MODAL_TYPES.CREATE_PASSWORD
2024-08-11 00:54:23 +02:00
? loc.settings.password_explain
: modalType === MODAL_TYPES.CREATE_FAKE_STORAGE
? `${loc.settings.password_explain} ${loc.plausibledeniability.create_password_explanation}`
: loc._.enter_password}
2024-08-05 00:08:01 +02:00
</Text>
<View style={styles.inputContainer}>
<Animated.View style={{ transform: [{ translateX: shakeAnimation }] }}>
<TextInput
testID="PasswordInput"
ref={passwordInputRef}
secureTextEntry
placeholder="Password"
value={password}
onChangeText={setPassword}
style={[styles.input, stylesHook.input]}
2024-08-13 18:38:38 +02:00
clearTextOnFocus
clearButtonMode="while-editing"
2024-08-05 00:08:01 +02:00
autoFocus
/>
</Animated.View>
2024-08-11 00:54:23 +02:00
{(modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) && (
2024-08-05 00:08:01 +02:00
<Animated.View style={{ transform: [{ translateX: shakeAnimation }] }}>
<TextInput
testID="ConfirmPasswordInput"
ref={confirmPasswordInputRef}
secureTextEntry
placeholder="Confirm Password"
value={confirmPassword}
2024-08-13 18:38:38 +02:00
clearTextOnFocus
clearButtonMode="while-editing"
2024-08-05 00:08:01 +02:00
onChangeText={setConfirmPassword}
style={[styles.input, stylesHook.input]}
/>
</Animated.View>
)}
</View>
</>
)}
2024-08-02 19:08:14 +02:00
</Animated.View>
)}
{isSuccess && (
<Animated.View
style={{
opacity: fadeInAnimation,
transform: [{ scale: scaleAnimation }],
}}
>
<View style={styles.successContainer}>
<View style={styles.circle}>
2024-08-03 02:51:22 +02:00
<Animated.Text
style={[
styles.checkmark,
{
transform: [
{
scale: scaleAnimation.interpolate({
2024-08-05 00:08:01 +02:00
inputRange: [0.8, 1],
2024-08-03 02:51:22 +02:00
outputRange: [0.8, 1],
}),
},
],
},
]}
>
</Animated.Text>
2024-08-02 19:08:14 +02:00
</View>
</View>
</Animated.View>
)}
</BottomModal>
);
},
);
export default PromptPasswordConfirmationModal;
const styles = StyleSheet.create({
modalContent: {
padding: 22,
width: '100%', // Ensure modal content takes full width
2024-08-02 19:08:14 +02:00
justifyContent: 'center',
alignItems: 'center',
},
2024-08-24 19:35:13 +02:00
minHeight: {
minHeight: 350,
2024-08-24 19:35:13 +02:00
},
2024-08-02 19:08:14 +02:00
feeModalFooter: {
2024-08-24 19:35:13 +02:00
paddingHorizontal: 16,
2024-08-02 19:08:14 +02:00
},
feeModalFooterSpacing: {
paddingHorizontal: 24,
},
inputContainer: {
marginBottom: 10,
width: '100%', // Ensure full width
},
input: {
borderRadius: 4,
padding: 8,
marginVertical: 8,
fontSize: 16,
width: '100%', // Ensure full width
},
textLabel: {
fontSize: 22,
fontWeight: '600',
marginBottom: 16,
textAlign: 'center',
},
2024-08-05 00:08:01 +02:00
description: {
fontSize: 16,
marginBottom: 12,
textAlign: 'center',
},
2024-08-02 19:08:14 +02:00
successContainer: {
justifyContent: 'center',
alignItems: 'center',
height: 100,
2024-08-02 19:08:14 +02:00
},
circle: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'green',
justifyContent: 'center',
alignItems: 'center',
},
checkmark: {
color: 'white',
fontSize: 30,
},
2024-08-02 19:50:51 +02:00
});