fix: animations got lost (#8504)
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

This commit is contained in:
Nuno 2026-04-27 16:40:50 +02:00 committed by GitHub
parent f87ffa9633
commit 038cabedaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 128 additions and 57 deletions

View File

@ -45,27 +45,27 @@ export const PasswordInput = forwardRef<PasswordInputHandle, PasswordInputProps>
// macOS-style shake animation - quick and snappy
Animated.sequence([
Animated.timing(shakeAnimation, {
toValue: 20,
duration: 80,
easing: Easing.linear,
toValue: 10,
duration: 50,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: -20,
duration: 80,
easing: Easing.linear,
toValue: -10,
duration: 50,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 20,
duration: 80,
easing: Easing.linear,
toValue: 8,
duration: 45,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
toValue: 0,
duration: 80,
easing: Easing.linear,
duration: 45,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start(() => {

View File

@ -175,8 +175,6 @@ const ToolTipMenu = (props: ToolTipMenuProps) => {
<Pressable
android_ripple={enableAndroidRipple ? { color: '#d9d9d9', foreground: true } : undefined}
style={({ pressed }) => {
// Keep visual feedback on Android by default. iOS context-menu preview
// already applies a system press effect; opt in when needed.
const shouldApplyPressedStyle =
pressed && ((Platform.OS === 'android' && enableAndroidRipple) || (Platform.OS === 'ios' && enableIOSPressOpacity));
return StyleSheet.flatten([styles.pressable, style, buttonStyle, shouldApplyPressedStyle && styles.pressed]);

View File

@ -1,7 +1,7 @@
import React, { useCallback, useMemo, memo } from 'react';
import React, { useCallback, useMemo, memo, useRef } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import { Linking, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native';
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native';
import Lnurl from '../class/lnurl';
import { LightningTransaction, Transaction } from '../class/wallets/types';
import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon';
@ -28,11 +28,6 @@ import { uint8ArrayToHex } from '../blue_modules/uint8array-extras';
import ListItem from './ListItem';
const styles = StyleSheet.create({
pressable: {
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
width: '100%',
},
dateLine: {
fontSize: 13,
},
@ -70,8 +65,45 @@ const styles = StyleSheet.create({
rightTitle: {
textAlign: 'right',
},
animatedScaleContainer: {
width: '100%',
},
});
type AnimatedPressableRowProps = {
onPress: () => void;
children: React.ReactNode;
accessibilityLabel: string;
};
const AnimatedPressableRow: React.FC<AnimatedPressableRowProps> = ({ onPress, children, accessibilityLabel }) => {
const scaleAnim = useRef(new Animated.Value(1)).current;
const animateTo = useCallback(
(toValue: number) => {
Animated.timing(scaleAnim, {
toValue,
duration: 120,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
},
[scaleAnim],
);
return (
<Pressable
onPress={onPress}
onPressIn={() => animateTo(0.97)}
onPressOut={() => animateTo(1)}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
>
<Animated.View style={[styles.animatedScaleContainer, { transform: [{ scale: scaleAnim }] }]}>{children}</Animated.View>
</Pressable>
);
};
interface TransactionListItemProps {
itemPriceUnit?: BitcoinUnit;
walletID: string;
@ -443,45 +475,45 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
return (
<ToolTipMenu
isButton
actions={toolTipActions}
onPressMenuItem={onToolTipPress}
onPress={onPress}
shouldOpenOnLongPress
buttonStyle={styles.fullWidthButton}
style={styles.fullWidthButton}
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
accessibilityRole="button"
>
{/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */}
<ListItem
leftAvatar={avatar}
title={listTitle}
subtitle={<Text style={styles.dateLine}>{dateLine}</Text>}
chevron={false}
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}
rightSubtitle={noteForCopy}
rightSubtitleStyle={styles.rightColumn}
containerStyle={combinedStyle}
testID="TransactionListItem"
accessibilityRole="button"
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
>
<View style={styles.row}>
<View style={styles.avatarContainer}>{avatar}</View>
<View style={styles.textContainer}>
<Text style={[styles.title, titleStyle]} numberOfLines={1}>
{title}
</Text>
{subtitleContent}
<AnimatedPressableRow onPress={onPress} accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}>
{/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */}
<ListItem
leftAvatar={avatar}
title={listTitle}
subtitle={<Text style={styles.dateLine}>{dateLine}</Text>}
chevron={false}
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}
rightSubtitle={noteForCopy}
rightSubtitleStyle={styles.rightColumn}
containerStyle={combinedStyle}
testID="TransactionListItem"
accessibilityRole="button"
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
>
<View style={styles.row}>
<View style={styles.avatarContainer}>{avatar}</View>
<View style={styles.textContainer}>
<Text style={[styles.title, titleStyle]} numberOfLines={1}>
{title}
</Text>
{subtitleContent}
</View>
<View style={styles.rightColumn}>
<Text style={[styles.rightTitle, rowTitleStyle]} numberOfLines={1}>
{rowTitle}
</Text>
</View>
</View>
<View style={styles.rightColumn}>
<Text style={[styles.rightTitle, rowTitleStyle]} numberOfLines={1}>
{rowTitle}
</Text>
</View>
</View>
</ListItem>
</ListItem>
</AnimatedPressableRow>
</ToolTipMenu>
);
},

View File

@ -1,9 +1,11 @@
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import {
ActivityIndicator,
Animated,
Dimensions,
Easing,
Image,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
TouchableWithoutFeedback,
@ -80,6 +82,7 @@ function reducer(state: State, action: Action): State {
const UnlockWith: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const isUnlockingWallets = useRef(false);
const keyboardOffset = useRef(new Animated.Value(0)).current;
const passwordInputRef = useRef<PasswordInputHandle>(null);
const passwordResolveRef = useRef<((password: string | undefined) => void) | null>(null);
const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useStorage();
@ -89,6 +92,44 @@ const UnlockWith: React.FC = () => {
setWalletsInitialized(false);
}, [setWalletsInitialized]);
useEffect(() => {
const windowHeight = Dimensions.get('window').height;
const animateToKeyboardPosition = (event: any, fallbackDuration = 220) => {
const keyboardTop = event?.endCoordinates?.screenY ?? windowHeight;
const keyboardHeight = Math.max(0, windowHeight - keyboardTop);
const target = -Math.min(Math.max(keyboardHeight * 0.28, 0), 96);
Animated.timing(keyboardOffset, {
toValue: target,
duration: event?.duration ?? fallbackDuration,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
};
const resetPosition = (event?: any) => {
Animated.timing(keyboardOffset, {
toValue: 0,
duration: event?.duration ?? 220,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
};
const subscriptions =
Platform.OS === 'ios'
? [
Keyboard.addListener('keyboardWillChangeFrame', animateToKeyboardPosition),
Keyboard.addListener('keyboardWillHide', resetPosition),
]
: [Keyboard.addListener('keyboardDidShow', animateToKeyboardPosition), Keyboard.addListener('keyboardDidHide', resetPosition)];
return () => {
subscriptions.forEach(sub => sub.remove());
};
}, [keyboardOffset]);
const successfullyAuthenticated = useCallback(() => {
setWalletsInitialized(true);
isUnlockingWallets.current = false;
@ -244,14 +285,14 @@ const UnlockWith: React.FC = () => {
return (
<SafeArea style={styles.root}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<KeyboardAvoidingView style={styles.keyboardAvoidingView} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<View style={styles.contentContainer}>
<View style={styles.keyboardAvoidingView}>
<Animated.View style={[styles.contentContainer, { transform: [{ translateY: keyboardOffset }] }]}>
<View style={styles.logoContainer}>
<Image source={require('../img/icon.png')} style={styles.logoImage} resizeMode="contain" />
</View>
<View style={styles.biometricRow}>{renderUnlockOptions()}</View>
</View>
</KeyboardAvoidingView>
</Animated.View>
</View>
</TouchableWithoutFeedback>
</SafeArea>
);