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 ) ;
2024-08-13 19:22:06 +02:00
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 ;
2024-08-13 19:22:06 +02:00
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 ,
2024-08-13 19:22:06 +02:00
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 ,
2024-08-13 19:22:06 +02:00
duration : 100 ,
2024-08-05 00:08:01 +02:00
easing : Easing.inOut ( Easing . ease ) ,
useNativeDriver : true ,
} ) ,
Animated . timing ( shakeAnimation , {
toValue : 5 ,
2024-08-13 19:22:06 +02:00
duration : 100 ,
2024-08-05 00:08:01 +02:00
easing : Easing.inOut ( Easing . ease ) ,
useNativeDriver : true ,
} ) ,
Animated . timing ( shakeAnimation , {
toValue : - 5 ,
2024-08-13 19:22:06 +02:00
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 ,
2024-08-13 19:22:06 +02:00
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%' ,
} ;
2024-08-11 23:46:16 +02:00
const onModalDismiss = ( ) = > {
resetState ( ) ;
onConfirmationFailure ( ) ;
2024-08-12 07:00:09 +02:00
} ;
2024-08-11 23:46:16 +02:00
2024-08-02 19:08:14 +02:00
return (
< BottomModal
ref = { modalRef }
2024-08-11 23:46:16 +02:00
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 } } >
2024-08-13 19:24:49 +02:00
< Text style = { [ styles . textLabel , stylesHook . feeModalLabel ] } > { loc . settings . encrypt_storage_explanation_headline } < / Text >
2024-08-13 19:22:06 +02:00
< 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 ] } >
2024-08-13 19:22:06 +02:00
{ 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 ] } >
2024-08-13 19:22:06 +02:00
{ modalType === MODAL_TYPES . CREATE_PASSWORD
2024-08-11 00:54:23 +02:00
? loc . settings . password_explain
2024-08-13 19:22:06 +02:00
: 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 ,
2024-08-11 21:58:18 +02:00
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 : {
2024-08-24 23:58:51 +02:00
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' ,
2024-08-13 19:22:06 +02:00
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
} ) ;