Add clipboard countdown context to keep global track of copied field id (#881)

This commit is contained in:
Leendert de Borst
2025-08-18 17:15:02 +02:00
parent bed2c78964
commit 634b7cada1
5 changed files with 110 additions and 26 deletions

View File

@@ -79,6 +79,8 @@ export default function SettingsScreen() : React.ReactNode {
display = t('settings.clipboardClearOptions.10seconds');
} else if (clipboardTimeout === 15) {
display = t('settings.clipboardClearOptions.15seconds');
} else if (clipboardTimeout === 30) {
display = t('settings.clipboardClearOptions.30seconds');
}
setClipboardClearDisplay(display);
@@ -101,7 +103,7 @@ export default function SettingsScreen() : React.ReactNode {
};
loadData();
}, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, t])
}, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, getClipboardClearTimeout, t])
);
/**

View File

@@ -14,6 +14,7 @@ import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
import { ThemedView } from '@/components/themed/ThemedView';
import { AliasVaultToast } from '@/components/Toast';
import { AuthProvider } from '@/context/AuthContext';
import { ClipboardCountdownProvider } from '@/context/ClipboardCountdownContext';
import { DbProvider } from '@/context/DbContext';
import { WebApiProvider } from '@/context/WebApiContext';
import { initI18n } from '@/i18n';
@@ -182,7 +183,9 @@ export default function RootLayout() : React.ReactNode {
<DbProvider>
<AuthProvider>
<WebApiProvider>
<RootLayoutNav />
<ClipboardCountdownProvider>
<RootLayoutNav />
</ClipboardCountdownProvider>
</WebApiProvider>
</AuthProvider>
</DbProvider>

View File

@@ -8,6 +8,7 @@ import Toast from 'react-native-toast-message';
import { useColors } from '@/hooks/useColorScheme';
import { useClipboardCountdown } from '@/context/ClipboardCountdownContext';
import NativeVaultManager from '@/specs/NativeVaultManager';
type FormInputCopyToClipboardProps = {
@@ -27,16 +28,50 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const colors = useColors();
const { t } = useTranslation();
const { activeFieldId, setActiveField } = useClipboardCountdown();
const animatedWidth = useRef(new Animated.Value(0)).current;
const [isCountingDown, setIsCountingDown] = useState(false);
// Create a stable unique ID based on label and value
const fieldId = useRef(`${label}-${value}-${Math.random().toString(36).substring(2, 11)}`).current;
const isCountingDown = activeFieldId === fieldId;
useEffect(() => {
return () => {
return (): void => {
// Cleanup on unmount
animatedWidth.stopAnimation();
};
}, [animatedWidth]);
useEffect(() => {
/* Handle animation based on whether this field is active */
if (isCountingDown) {
// This field is now active - reset and start animation
animatedWidth.stopAnimation();
animatedWidth.setValue(100);
// Get timeout and start animation
AsyncStorage.getItem('clipboard_clear_timeout').then((timeoutStr) => {
const timeoutSeconds = timeoutStr ? parseInt(timeoutStr, 10) : 10;
if (timeoutSeconds > 0 && activeFieldId === fieldId) {
Animated.timing(animatedWidth, {
toValue: 0,
duration: timeoutSeconds * 1000,
useNativeDriver: false,
easing: Easing.linear,
}).start((finished) => {
if (finished && activeFieldId === fieldId) {
setActiveField(null);
}
});
}
});
} else {
// This field is not active - stop animation and reset
animatedWidth.stopAnimation();
animatedWidth.setValue(0);
}
}, [isCountingDown, activeFieldId, fieldId, animatedWidth, setActiveField]);
/**
* Copy the value to the clipboard.
*/
@@ -52,20 +87,19 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
// Schedule clipboard clear if timeout is set
if (timeoutSeconds > 0) {
// Clear any existing active field first (this will cancel its animation)
setActiveField(null);
// Schedule the clipboard clear
await NativeVaultManager.clearClipboardAfterDelay(timeoutSeconds);
// Start countdown animation
setIsCountingDown(true);
animatedWidth.setValue(100);
Animated.timing(animatedWidth, {
toValue: 0,
duration: timeoutSeconds * 1000,
useNativeDriver: false,
easing: Easing.linear,
}).start(() => {
setIsCountingDown(false);
});
/*
* Now set this field as active - animation will be handled by the effect
* Use setTimeout to ensure state update happens in next tick
*/
setTimeout(() => {
setActiveField(fieldId);
}, 0);
}
if (Platform.OS !== 'android') {
@@ -98,6 +132,14 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
alignItems: 'center',
flexDirection: 'row',
},
animatedOverlay: {
backgroundColor: `${colors.primary}50`,
borderRadius: 8,
bottom: 0,
left: 0,
position: 'absolute',
top: 0,
},
iconButton: {
padding: 8,
},
@@ -105,12 +147,14 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
backgroundColor: colors.accentBackground,
borderRadius: 8,
marginBottom: 8,
padding: 12,
overflow: 'hidden',
position: 'relative',
},
inputContent: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
padding: 12,
},
label: {
color: colors.textMuted,
@@ -122,14 +166,6 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
fontSize: 16,
fontWeight: '500',
},
animatedOverlay: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: `${colors.primary}50`,
borderRadius: 8,
},
});
return (

View File

@@ -0,0 +1,42 @@
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
type ClipboardCountdownContextType = {
activeFieldId: string | null;
setActiveField: (fieldId: string | null | ((prev: string | null) => string | null)) => void;
}
const ClipboardCountdownContext = createContext<ClipboardCountdownContextType | undefined>(undefined);
/**
* Clipboard countdown context provider.
*/
export const ClipboardCountdownProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
const setActiveField = useCallback((fieldId: string | null | ((prev: string | null) => string | null)) => {
if (typeof fieldId === 'function') {
setActiveFieldId(fieldId);
} else {
setActiveFieldId(fieldId);
}
}, []);
const value = useMemo(() => ({ activeFieldId, setActiveField }), [activeFieldId, setActiveField]);
return (
<ClipboardCountdownContext.Provider value={value}>
{children}
</ClipboardCountdownContext.Provider>
);
};
/**
* Clipboard countdown context hook.
*/
export const useClipboardCountdown = (): ClipboardCountdownContextType => {
const context = useContext(ClipboardCountdownContext);
if (!context) {
throw new Error('useClipboardCountdown must be used within ClipboardCountdownProvider');
}
return context;
};

View File

@@ -199,7 +199,8 @@
"never": "Never",
"5seconds": "5 seconds",
"10seconds": "10 seconds",
"15seconds": "15 seconds"
"15seconds": "15 seconds",
"30seconds": "30 seconds"
},
"identityGenerator": "Identity Generator",
"security": "Security",