BlueWallet/components/PasswordInput.tsx
Nuno 038cabedaf
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
fix: animations got lost (#8504)
2026-04-27 16:40:50 +02:00

218 lines
5.9 KiB
TypeScript

import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Animated, Easing, StyleSheet, TextInput, View } from 'react-native';
import { useTheme } from './themes';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc';
export interface PasswordInputHandle {
focus: () => void;
blur: () => void;
clear: () => void;
showError: () => void;
showSuccess: () => void;
reset: () => void;
getValue: () => string;
}
interface PasswordInputProps {
onSubmit: (password: string) => void;
placeholder?: string;
disabled?: boolean;
onChangeText?: (text: string) => void;
}
export const PasswordInput = forwardRef<PasswordInputHandle, PasswordInputProps>(
({ onSubmit, placeholder = loc._.enter_password, disabled = false, onChangeText }, ref) => {
const [password, setPassword] = useState('');
const [isSuccess, setIsSuccess] = useState(false);
const inputRef = useRef<TextInput>(null);
const shakeAnimation = useRef(new Animated.Value(0)).current;
const checkmarkScale = useRef(new Animated.Value(0)).current;
const checkmarkOpacity = useRef(new Animated.Value(0)).current;
const { colors } = useTheme();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
blur: () => inputRef.current?.blur(),
clear: () => setPassword(''),
getValue: () => password,
showError: () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
setIsSuccess(false);
// macOS-style shake animation - quick and snappy
Animated.sequence([
Animated.timing(shakeAnimation, {
toValue: 10,
duration: 50,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -10,
duration: 50,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 8,
duration: 45,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 0,
duration: 45,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start(() => {
// Clear password after shake
setPassword('');
});
},
showSuccess: () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
setIsSuccess(true);
// Dismiss keyboard on success
inputRef.current?.blur();
// Quick pop-in animation for checkmark
checkmarkScale.setValue(0);
checkmarkOpacity.setValue(0);
Animated.parallel([
Animated.spring(checkmarkScale, {
toValue: 1,
tension: 100,
friction: 5,
useNativeDriver: true,
}),
Animated.timing(checkmarkOpacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
]).start();
},
reset: () => {
setPassword('');
setIsSuccess(false);
shakeAnimation.setValue(0);
checkmarkScale.setValue(0);
checkmarkOpacity.setValue(0);
},
}));
const handleSubmit = () => {
if (password.trim() && !isSuccess) {
onSubmit(password);
}
};
const stylesHook = StyleSheet.create({
container: {
borderColor: isSuccess ? colors.successColor : colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
input: {
color: colors.foregroundColor,
},
checkmark: {
color: colors.successColor,
},
});
return (
<Animated.View
style={[
styles.container,
stylesHook.container,
{
transform: [{ translateX: shakeAnimation }],
},
]}
>
<TextInput
ref={inputRef}
testID="PasswordInput"
style={[styles.input, stylesHook.input]}
value={password}
onChangeText={text => {
setPassword(text);
onChangeText?.(text);
}}
clearButtonMode={isSuccess ? 'never' : 'while-editing'}
placeholder={placeholder}
placeholderTextColor={colors.alternativeTextColor}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
editable={!isSuccess}
onSubmitEditing={handleSubmit}
returnKeyType="done"
enablesReturnKeyAutomatically={true}
/>
{isSuccess && (
<Animated.View
style={[
styles.checkmarkContainer,
{
opacity: checkmarkOpacity,
transform: [{ scale: checkmarkScale }],
},
]}
pointerEvents="none"
>
<View style={styles.checkmarkCircle}>
<View style={[styles.checkmark, { borderColor: colors.successColor }]} />
</View>
</Animated.View>
)}
</Animated.View>
);
},
);
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 8,
borderWidth: 2,
paddingHorizontal: 16,
minHeight: 54,
width: '100%',
},
input: {
flex: 1,
fontSize: 16,
paddingVertical: 12,
},
checkmarkContainer: {
marginLeft: 12,
justifyContent: 'center',
alignItems: 'center',
},
checkmarkCircle: {
width: 24,
height: 24,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
checkmark: {
width: 8,
height: 14,
borderBottomWidth: 3,
borderRightWidth: 3,
transform: [{ rotate: '45deg' }, { translateY: -2 }],
},
});
PasswordInput.displayName = 'PasswordInput';