Fix app initialize and reinitialize layout (#1274)

This commit is contained in:
Leendert de Borst
2025-09-29 13:31:22 +02:00
parent 5eb28d3ddf
commit 75eea4162d
3 changed files with 115 additions and 104 deletions

View File

@@ -1,8 +1,10 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useEffect, useRef, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, StyleSheet } from 'react-native';
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useColors } from '@/hooks/useColorScheme';
import { useVaultSync } from '@/hooks/useVaultSync';
import LoadingIndicator from '@/components/LoadingIndicator';
@@ -17,13 +19,14 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
export default function Initialize() : React.ReactNode {
const router = useRouter();
const [status, setStatus] = useState('');
const [showOfflineButton, setShowOfflineButton] = useState(false);
const [showSkipButton, setShowSkipButton] = useState(false);
const hasInitialized = useRef(false);
const offlineButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const skipButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const { t } = useTranslation();
const app = useApp();
const { syncVault } = useVaultSync();
const dbContext = useDb();
const colors = useColors();
/**
* Handle offline scenario - show alert with options to open local vault or retry sync.
@@ -97,12 +100,12 @@ export default function Initialize() : React.ReactNode {
*/
onPress: () : void => {
setStatus(t('app.status.retryingConnection'));
setShowOfflineButton(false);
setShowSkipButton(false);
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
offlineButtonTimeoutRef.current = null;
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
skipButtonTimeoutRef.current = null;
}
/**
@@ -195,17 +198,16 @@ export default function Initialize() : React.ReactNode {
setStatus(message);
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
skipButtonTimeoutRef.current = null;
}
// Show offline button after 2 seconds if we're checking vault updates
if (message === t('vault.checkingVaultUpdates')) {
offlineButtonTimeoutRef.current = setTimeout(() => {
setShowOfflineButton(true);
}, 2000) as unknown as NodeJS.Timeout;
} else {
setShowOfflineButton(false);
// Show skip button after 5 seconds when we start loading
if (message && !showSkipButton) {
skipButtonTimeoutRef.current = setTimeout(() => {
setShowSkipButton(true);
}, 5000) as unknown as NodeJS.Timeout;
}
},
/**
@@ -247,22 +249,22 @@ export default function Initialize() : React.ReactNode {
// Cleanup timeout on unmount
return (): void => {
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
}
};
}, [dbContext, syncVault, app, router, t, handleOfflineFlow]);
}, [dbContext, syncVault, app, router, t, handleOfflineFlow, showSkipButton]);
/**
* Handle offline button press by calling the stored offline handler.
* Handle skip button press by calling the offline handler.
*/
const handleOfflinePress = (): void => {
const handleSkipPress = (): void => {
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
}
setShowOfflineButton(false);
setShowSkipButton(false);
handleOfflineFlow();
};
@@ -272,18 +274,37 @@ export default function Initialize() : React.ReactNode {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
paddingHorizontal: 20,
},
skipButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.accentBackground,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 8,
width: 200,
borderWidth: 1,
borderColor: colors.accentBorder,
},
skipButtonText: {
marginLeft: 8,
fontSize: 16,
color: colors.textMuted,
},
});
return (
<ThemedView style={styles.container}>
{status ? (
<LoadingIndicator
status={status}
showOfflineButton={showOfflineButton}
onOfflinePress={handleOfflinePress}
/>
) : null}
<View>
<LoadingIndicator status={status || ''} />
</View>
{showSkipButton && (
<TouchableOpacity style={styles.skipButton} onPress={handleSkipPress}>
<Ionicons name="close" size={20} color={colors.textMuted} />
</TouchableOpacity>
)}
</ThemedView>
);
}

View File

@@ -1,7 +1,8 @@
import { Ionicons } from '@expo/vector-icons';
import { Href, router } from 'expo-router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View, Alert } from 'react-native';
import { StyleSheet, View, Alert, TouchableOpacity } from 'react-native';
import { useColors } from '@/hooks/useColorScheme';
import { useVaultSync } from '@/hooks/useVaultSync';
@@ -22,9 +23,9 @@ export default function ReinitializeScreen() : React.ReactNode {
const dbContext = useDb();
const { syncVault } = useVaultSync();
const [status, setStatus] = useState('');
const [showOfflineButton, setShowOfflineButton] = useState(false);
const [showSkipButton, setShowSkipButton] = useState(false);
const hasInitialized = useRef(false);
const offlineButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const skipButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const colors = useColors();
const { t } = useTranslation();
@@ -109,11 +110,11 @@ export default function ReinitializeScreen() : React.ReactNode {
router.replace('/(tabs)/credentials');
setTimeout(() => {
if (params.serviceUrl) {
router.push(path + '?serviceUrl=' + params.serviceUrl);
router.push(`${path}?serviceUrl=${params.serviceUrl}` as Href);
} else if (params.id) {
router.push(path + '?id=' + params.id);
router.push(`${path}?id=${params.id}` as Href);
} else {
router.push(path);
router.push(path as Href);
}
}, 0);
app.setReturnUrl(null);
@@ -130,12 +131,12 @@ export default function ReinitializeScreen() : React.ReactNode {
*/
onPress: () : void => {
setStatus(t('app.status.retryingConnection'));
setShowOfflineButton(false);
setShowSkipButton(false);
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
offlineButtonTimeoutRef.current = null;
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
skipButtonTimeoutRef.current = null;
}
/**
@@ -180,11 +181,11 @@ export default function ReinitializeScreen() : React.ReactNode {
const params = app.returnUrl.params as Record<string, string>;
if (params.serviceUrl) {
simulateStackNavigation('/(tabs)/credentials', path + '?serviceUrl=' + params.serviceUrl);
simulateStackNavigation('/(tabs)/credentials', `${path}?serviceUrl=${params.serviceUrl}`);
} else if (params.id) {
simulateStackNavigation('/(tabs)/credentials', path + '?id=' + params.id);
simulateStackNavigation('/(tabs)/credentials', `${path}?id=${params.id}`);
} else {
simulateStackNavigation('/(tabs)/credentials', path);
simulateStackNavigation('/(tabs)/credentials', path as string);
}
} else {
router.replace({
@@ -267,18 +268,11 @@ export default function ReinitializeScreen() : React.ReactNode {
onStatus: (message) => {
setStatus(message);
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
}
// Show offline button after 2 seconds if we're checking vault updates
if (message === t('vault.checkingVaultUpdates')) {
offlineButtonTimeoutRef.current = setTimeout(() => {
setShowOfflineButton(true);
}, 2000) as unknown as NodeJS.Timeout;
} else {
setShowOfflineButton(false);
// Show skip button after 5 seconds when we start loading
if (message && !skipButtonTimeoutRef.current) {
skipButtonTimeoutRef.current = setTimeout(() => {
setShowSkipButton(true);
}, 5000) as unknown as NodeJS.Timeout;
}
},
/**
@@ -312,25 +306,18 @@ export default function ReinitializeScreen() : React.ReactNode {
};
initialize();
// Cleanup timeout on unmount
return (): void => {
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
}
};
}, [syncVault, app, dbContext, t, handleOfflineFlow]);
/**
* Handle offline button press by calling the stored offline handler.
* Handle skip button press by calling the offline handler.
*/
const handleOfflinePress = (): void => {
const handleSkipPress = (): void => {
// Clear any existing timeout
if (offlineButtonTimeoutRef.current) {
clearTimeout(offlineButtonTimeoutRef.current);
if (skipButtonTimeoutRef.current) {
clearTimeout(skipButtonTimeoutRef.current);
}
setShowOfflineButton(false);
setShowSkipButton(false);
handleOfflineFlow();
};
@@ -340,6 +327,11 @@ export default function ReinitializeScreen() : React.ReactNode {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
paddingHorizontal: 20,
},
contentWrapper: {
alignItems: 'center',
width: '100%',
},
message1: {
marginTop: 5,
@@ -347,26 +339,48 @@ export default function ReinitializeScreen() : React.ReactNode {
},
message2: {
textAlign: 'center',
marginBottom: 10,
},
messageContainer: {
backgroundColor: colors.accentBackground,
borderRadius: 10,
padding: 20,
alignItems: 'center',
width: '100%',
maxWidth: 300,
},
skipButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.accentBackground,
paddingVertical: 8,
paddingHorizontal: 20,
borderRadius: 8,
width: 200,
borderWidth: 1,
borderColor: colors.accentBorder,
},
skipButtonText: {
marginLeft: 8,
fontSize: 16,
color: colors.textMuted,
},
});
return (
<ThemedView style={styles.container}>
<View style={styles.messageContainer}>
<ThemedText style={styles.message1}>{t('app.reinitialize.vaultAutoLockedMessage')}</ThemedText>
<ThemedText style={styles.message2}>{t('app.reinitialize.attemptingToUnlockMessage')}</ThemedText>
{status ? (
<LoadingIndicator
status={status}
showOfflineButton={showOfflineButton}
onOfflinePress={handleOfflinePress}
/>
) : null}
<View style={styles.contentWrapper}>
<View style={styles.messageContainer}>
<ThemedText style={styles.message1}>{t('app.reinitialize.vaultAutoLockedMessage')}</ThemedText>
<ThemedText style={styles.message2}>{t('app.reinitialize.attemptingToUnlockMessage')}</ThemedText>
{status ? <LoadingIndicator status={status} /> : null}
{showSkipButton && (
<TouchableOpacity style={styles.skipButton} onPress={handleSkipPress}>
<Ionicons name="close" size={20} color={colors.textMuted} />
</TouchableOpacity>
)}
</View>
</View>
</ThemedView>
);

View File

@@ -1,19 +1,16 @@
import { useEffect, useRef, useState } from 'react';
import { StyleSheet, View, Text, Animated, useColorScheme, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, View, Text, Animated, useColorScheme } from 'react-native';
import { useColors } from '@/hooks/useColorScheme';
type LoadingIndicatorProps = {
status: string;
showOfflineButton?: boolean;
onOfflinePress?: () => void;
};
/**
* Loading indicator component.
*/
export default function LoadingIndicator({ status, showOfflineButton, onOfflinePress }: LoadingIndicatorProps): React.ReactNode {
export default function LoadingIndicator({ status }: LoadingIndicatorProps): React.ReactNode {
const colors = useColors();
const dot1Anim = useRef(new Animated.Value(0)).current;
const dot2Anim = useRef(new Animated.Value(0)).current;
@@ -103,20 +100,6 @@ export default function LoadingIndicator({ status, showOfflineButton, onOfflineP
alignItems: 'center',
justifyContent: 'center',
padding: 20,
...StyleSheet.absoluteFillObject,
},
closeIconContainer: {
position: 'absolute',
right: 20,
top: 60,
zIndex: 10,
},
closeIcon: {
padding: 12,
borderRadius: 32,
backgroundColor: colors.accentBackground,
borderWidth: 2,
borderColor: colors.accentBorder,
},
contentContainer: {
alignItems: 'center',
@@ -156,13 +139,6 @@ export default function LoadingIndicator({ status, showOfflineButton, onOfflineP
return (
<View style={styles.container}>
{showOfflineButton && (
<View style={styles.closeIconContainer}>
<TouchableOpacity style={styles.closeIcon} onPress={onOfflinePress}>
<Ionicons name="close" size={24} color={colors.text} />
</TouchableOpacity>
</View>
)}
<View style={styles.contentContainer}>
<View style={styles.dotsContainer}>
<Animated.View