mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 15:41:40 -04:00
Add AliasVault themed loading indicators (#771)
This commit is contained in:
@@ -13,6 +13,7 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
import { CredentialCard } from '@/components/CredentialCard';
|
||||
import { TitleContainer } from '@/components/TitleContainer';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
export default function CredentialsScreen() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -82,8 +83,7 @@ export default function CredentialsScreen() {
|
||||
|
||||
// Sync vault and load credentials
|
||||
await syncVault({
|
||||
forceCheck: true,
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (hasNewVault) => {
|
||||
// Calculate remaining time needed to reach minimum duration
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const remainingDelay = Math.max(0, 350 - elapsedTime);
|
||||
@@ -95,14 +95,31 @@ export default function CredentialsScreen() {
|
||||
|
||||
await loadCredentials();
|
||||
setRefreshing(false);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: hasNewVault ? 'Vault synced successfully' : 'Vault is up-to-date',
|
||||
position: 'top',
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error syncing vault:', error);
|
||||
}
|
||||
setRefreshing(false);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Vault sync failed',
|
||||
text2: error,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error refreshing credentials:', err);
|
||||
setRefreshing(false);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Vault sync failed',
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [syncVault, loadCredentials]);
|
||||
|
||||
|
||||
@@ -30,6 +30,60 @@ const toastConfig = {
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 70,
|
||||
marginTop: 30,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: 'white', fontSize: 14, fontWeight: '500' }}>
|
||||
{props.text1}
|
||||
</Text>
|
||||
</View>
|
||||
),
|
||||
error: (props: any) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#dc2626', // Red
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 70,
|
||||
marginTop: 30,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: 'white', fontSize: 14, fontWeight: '500' }}>
|
||||
{props.text1}
|
||||
</Text>
|
||||
{props.text2 && (
|
||||
<Text style={{ color: 'white', fontSize: 12, marginTop: 4 }}>
|
||||
{props.text2}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
info: (props: any) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#3b82f6', // Blue
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 70,
|
||||
marginTop: 30,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
@@ -51,7 +105,6 @@ function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
const colors = useColors();
|
||||
|
||||
|
||||
// Create custom themes that extend the default ones.
|
||||
const customDefaultTheme = {
|
||||
...DefaultTheme,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { View, ActivityIndicator } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
|
||||
export default function InitialLoadingScreen() {
|
||||
const { isInitialized: isAuthInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable } = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
const hasInitialized = useRef(false);
|
||||
const [status, setStatus] = useState('Initializing...');
|
||||
|
||||
const isFullyInitialized = isAuthInitialized && dbInitialized;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isLoggedIn || !dbAvailable);
|
||||
@@ -25,7 +27,12 @@ export default function InitialLoadingScreen() {
|
||||
|
||||
// Perform initial vault sync
|
||||
console.log('Initial vault sync');
|
||||
await syncVault({ forceCheck: true });
|
||||
await syncVault({
|
||||
initialSync: true,
|
||||
onStatus: (message) => {
|
||||
setStatus(message);
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate to appropriate screen
|
||||
if (requireLoginOrUnlock) {
|
||||
@@ -42,7 +49,7 @@ export default function InitialLoadingScreen() {
|
||||
|
||||
return (
|
||||
<ThemedView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#f97316" />
|
||||
<LoadingIndicator status={status} />
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, ActivityIndicator, Linking, Animated } from 'react-native';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
@@ -15,6 +16,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import Logo from '@/assets/images/logo.svg';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
|
||||
|
||||
export default function LoginScreen() {
|
||||
@@ -196,6 +198,7 @@ export default function LoginScreen() {
|
||||
const [loginResponse, setLoginResponse] = useState<any>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
|
||||
const [loginStatus, setLoginStatus] = useState<string | null>(null);
|
||||
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
@@ -207,6 +210,8 @@ export default function LoginScreen() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
setLoginStatus('Logging in');
|
||||
|
||||
try {
|
||||
console.log('handleSubmit');
|
||||
authContext.clearGlobalMessage();
|
||||
@@ -229,6 +234,7 @@ export default function LoginScreen() {
|
||||
|
||||
console.log('passwordHashString', passwordHashString);
|
||||
|
||||
setLoginStatus('Validating credentials');
|
||||
const validationResponse = await srpUtil.validateLogin(
|
||||
credentials.username,
|
||||
passwordHashString,
|
||||
@@ -244,6 +250,7 @@ export default function LoginScreen() {
|
||||
setPasswordHashBase64(passwordHashBase64);
|
||||
setTwoFactorRequired(true);
|
||||
setIsLoading(false);
|
||||
setLoginStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -253,6 +260,7 @@ export default function LoginScreen() {
|
||||
|
||||
console.log('validationResponse.token', validationResponse.token);
|
||||
|
||||
setLoginStatus('Syncing vault');
|
||||
const vaultResponseJson = await webApi.authFetch<any>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
@@ -264,6 +272,7 @@ export default function LoginScreen() {
|
||||
console.error('vaultError', vaultError);
|
||||
setError(vaultError);
|
||||
setIsLoading(false);
|
||||
setLoginStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,6 +286,7 @@ export default function LoginScreen() {
|
||||
router.replace('/(tabs)');
|
||||
|
||||
setIsLoading(false);
|
||||
setLoginStatus(null);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiAuthError) {
|
||||
console.error('ApiAuthError error:', err);
|
||||
@@ -286,6 +296,7 @@ export default function LoginScreen() {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
setLoginStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -360,118 +371,124 @@ export default function LoginScreen() {
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
<ThemedView style={styles.content}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.headerTitle}>Log in</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Connecting to{' '}
|
||||
<Text
|
||||
style={styles.clickableDomain}
|
||||
onPress={() => router.push('/settings')}
|
||||
>
|
||||
{getDisplayUrl()}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{twoFactorRequired ? (
|
||||
<View style={styles.formContainer}>
|
||||
<Text style={[styles.label]}>Authentication Code</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={twoFactorCode}
|
||||
onChangeText={setTwoFactorCode}
|
||||
placeholder="Enter 6-digit code"
|
||||
keyboardType="numeric"
|
||||
maxLength={6}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleTwoFactorSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.text} />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Verify</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={() => {
|
||||
setCredentials({ username: '', password: '' });
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
setPasswordHashString(null);
|
||||
setPasswordHashBase64(null);
|
||||
setLoginResponse(null);
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.noteText]}>
|
||||
Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.
|
||||
</Text>
|
||||
</View>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator status={loginStatus || 'Loading...'} />
|
||||
) : (
|
||||
<View style={styles.formContainer}>
|
||||
<Text style={[styles.label]}>Username or email</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={credentials.username}
|
||||
onChangeText={(text) => setCredentials({ ...credentials, username: text })}
|
||||
placeholder="name / name@company.com"
|
||||
autoCapitalize="none"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={[styles.label]}>Password</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={credentials.password}
|
||||
onChangeText={(text) => setCredentials({ ...credentials, password: text })}
|
||||
placeholder="Enter your password"
|
||||
secureTextEntry
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<View style={styles.rememberMeContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.checkbox]}
|
||||
onPress={() => setRememberMe(!rememberMe)}
|
||||
>
|
||||
<View style={[styles.checkboxInner, rememberMe && styles.checkboxChecked]} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.rememberMeText]}>Remember me</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.text} />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Login</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.noteText]}>
|
||||
No account yet?{' '}
|
||||
<Text
|
||||
style={styles.clickableDomain}
|
||||
onPress={() => Linking.openURL('https://app.aliasvault.net')}
|
||||
>
|
||||
Create new vault
|
||||
<>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.headerTitle}>Log in</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Connecting to{' '}
|
||||
<Text
|
||||
style={styles.clickableDomain}
|
||||
onPress={() => router.push('/settings')}
|
||||
>
|
||||
{getDisplayUrl()}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{twoFactorRequired ? (
|
||||
<View style={styles.formContainer}>
|
||||
<Text style={[styles.label]}>Authentication Code</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={twoFactorCode}
|
||||
onChangeText={setTwoFactorCode}
|
||||
placeholder="Enter 6-digit code"
|
||||
keyboardType="numeric"
|
||||
maxLength={6}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleTwoFactorSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.text} />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Verify</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={() => {
|
||||
setCredentials({ username: '', password: '' });
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
setPasswordHashString(null);
|
||||
setPasswordHashBase64(null);
|
||||
setLoginResponse(null);
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.noteText]}>
|
||||
Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.formContainer}>
|
||||
<Text style={[styles.label]}>Username or email</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={credentials.username}
|
||||
onChangeText={(text) => setCredentials({ ...credentials, username: text })}
|
||||
placeholder="name / name@company.com"
|
||||
autoCapitalize="none"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={[styles.label]}>Password</Text>
|
||||
<TextInput
|
||||
style={[styles.input]}
|
||||
value={credentials.password}
|
||||
onChangeText={(text) => setCredentials({ ...credentials, password: text })}
|
||||
placeholder="Enter your password"
|
||||
secureTextEntry
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<View style={styles.rememberMeContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.checkbox]}
|
||||
onPress={() => setRememberMe(!rememberMe)}
|
||||
>
|
||||
<View style={[styles.checkboxInner, rememberMe && styles.checkboxChecked]} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.rememberMeText]}>Remember me</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={colors.text} />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Login</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.noteText]}>
|
||||
No account yet?{' '}
|
||||
<Text
|
||||
style={styles.clickableDomain}
|
||||
onPress={() => Linking.openURL('https://app.aliasvault.net')}
|
||||
>
|
||||
Create new vault
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ThemedView>
|
||||
</View>
|
||||
|
||||
156
mobile-app/components/LoadingIndicator.tsx
Normal file
156
mobile-app/components/LoadingIndicator.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { StyleSheet, View, Text, Animated } from 'react-native';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function LoadingIndicator({ status }: LoadingIndicatorProps) {
|
||||
const colors = useColors();
|
||||
const dot1Anim = useRef(new Animated.Value(0)).current;
|
||||
const dot2Anim = useRef(new Animated.Value(0)).current;
|
||||
const dot3Anim = useRef(new Animated.Value(0)).current;
|
||||
const dot4Anim = useRef(new Animated.Value(0)).current;
|
||||
const [dots, setDots] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const createPulseAnimation = (anim: Animated.Value) => {
|
||||
return Animated.sequence([
|
||||
Animated.timing(anim, {
|
||||
toValue: 1,
|
||||
duration: 700,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(anim, {
|
||||
toValue: 0,
|
||||
duration: 700,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const animation = Animated.loop(
|
||||
Animated.parallel([
|
||||
createPulseAnimation(dot1Anim),
|
||||
Animated.sequence([
|
||||
Animated.delay(200),
|
||||
createPulseAnimation(dot2Anim),
|
||||
]),
|
||||
Animated.sequence([
|
||||
Animated.delay(400),
|
||||
createPulseAnimation(dot3Anim),
|
||||
]),
|
||||
Animated.sequence([
|
||||
Animated.delay(600),
|
||||
createPulseAnimation(dot4Anim),
|
||||
]),
|
||||
])
|
||||
);
|
||||
|
||||
const dotsInterval = setInterval(() => {
|
||||
setDots(prevDots => {
|
||||
if (prevDots.length >= 3) return '';
|
||||
return prevDots + '.';
|
||||
});
|
||||
}, 400);
|
||||
|
||||
animation.start();
|
||||
|
||||
return () => {
|
||||
animation.stop();
|
||||
clearInterval(dotsInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
dotsContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 20,
|
||||
padding: 12,
|
||||
paddingHorizontal: 24,
|
||||
gap: 10,
|
||||
borderWidth: 5,
|
||||
borderColor: colors.tertiary,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.tertiary,
|
||||
},
|
||||
statusText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.dotsContainer}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
opacity: dot1Anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 1],
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
opacity: dot2Anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 1],
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
opacity: dot3Anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 1],
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
opacity: dot4Anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 1],
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.statusText}>{status}{dots}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -19,8 +19,9 @@ export const Colors = {
|
||||
tabIconSelected: '#f49541',
|
||||
headerBackground: '#fff',
|
||||
tabBarBackground: '#fff',
|
||||
primary: '#f97316',
|
||||
primary: '#f49541',
|
||||
secondary: '#6b7280',
|
||||
tertiary: '#eabf69',
|
||||
loginHeader: '#f6dfc4',
|
||||
},
|
||||
dark: {
|
||||
@@ -38,8 +39,9 @@ export const Colors = {
|
||||
tabIconSelected: '#f49541',
|
||||
headerBackground: '#1f2937',
|
||||
tabBarBackground: '#1f2937',
|
||||
primary: '#f97316',
|
||||
primary: '#f49541',
|
||||
secondary: '#6b7280',
|
||||
tertiary: '#eabf69',
|
||||
loginHeader: '#5c4331',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -5,10 +5,32 @@ import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
|
||||
// Utility function to ensure a minimum time has elapsed for an operation
|
||||
const withMinimumDelay = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
minDelayMs: number,
|
||||
initialSync: boolean
|
||||
): Promise<T> => {
|
||||
if (!initialSync) {
|
||||
return operation();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await operation();
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
|
||||
if (elapsedTime < minDelayMs) {
|
||||
await new Promise(resolve => setTimeout(resolve, minDelayMs - elapsedTime));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
interface VaultSyncOptions {
|
||||
forceCheck?: boolean;
|
||||
onSuccess?: () => void;
|
||||
initialSync?: boolean;
|
||||
onSuccess?: (hasNewVault: boolean) => void;
|
||||
onError?: (error: string) => void;
|
||||
onStatus?: (message: string) => void;
|
||||
}
|
||||
|
||||
export const useVaultSync = () => {
|
||||
@@ -17,8 +39,8 @@ export const useVaultSync = () => {
|
||||
const webApi = useWebApi();
|
||||
|
||||
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
|
||||
const { forceCheck = false, onSuccess, onError } = options;
|
||||
console.log('syncVault called with forceCheck:', forceCheck);
|
||||
const { initialSync = false, onSuccess, onError, onStatus } = options;
|
||||
console.log('syncVault called with initialSync:', initialSync);
|
||||
|
||||
try {
|
||||
const isLoggedIn = await authContext.initializeAuth();
|
||||
@@ -28,31 +50,23 @@ export const useVaultSync = () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not forcing a check, verify the time elapsed since last check
|
||||
if (!forceCheck) {
|
||||
const lastCheckStr = await AsyncStorage.getItem('lastVaultCheck');
|
||||
const lastCheck = lastCheckStr ? parseInt(lastCheckStr, 10) : 0;
|
||||
const now = Date.now();
|
||||
|
||||
// Only check if more than 1 hour has passed since last check
|
||||
if (now - lastCheck < 3600000) {
|
||||
console.log('Vault sync skipped: Not enough time has passed since last check');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Checking vault updates');
|
||||
|
||||
// Update last check time
|
||||
await AsyncStorage.setItem('lastVaultCheck', Date.now().toString());
|
||||
|
||||
// Check app status and vault revision
|
||||
const statusResponse = await webApi.getStatus();
|
||||
onStatus?.('Checking vault updates');
|
||||
const statusResponse = await withMinimumDelay(
|
||||
() => webApi.getStatus(),
|
||||
700,
|
||||
initialSync
|
||||
);
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
console.log('Vault sync error:', statusError);
|
||||
await webApi.logout(statusError);
|
||||
onError?.(statusErrorr);
|
||||
onError?.(statusError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,7 +77,12 @@ export const useVaultSync = () => {
|
||||
console.log('Vault revision local:', vaultRevisionNumber);
|
||||
console.log('Vault revision server:', statusResponse.vaultRevision);
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
onStatus?.('Syncing updated vault');
|
||||
const vaultResponseJson = await withMinimumDelay(
|
||||
() => webApi.get<VaultResponse>('Vault'),
|
||||
1000,
|
||||
initialSync
|
||||
);
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
|
||||
if (vaultError) {
|
||||
@@ -74,13 +93,15 @@ export const useVaultSync = () => {
|
||||
}
|
||||
|
||||
console.log('Re-initializing database with new vault');
|
||||
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, null);
|
||||
onSuccess?.();
|
||||
dbContext.initializeDatabase(vaultResponseJson as VaultResponse, null);
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Vault sync finished: No updates needed');
|
||||
onSuccess?.();
|
||||
onStatus?.('Decrypting vault');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
onSuccess?.(false);
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
|
||||
|
||||
Reference in New Issue
Block a user