diff --git a/apps/mobile-app/.eslintrc.js b/apps/mobile-app/.eslintrc.js
index f2d7c975f..ddf37ed27 100644
--- a/apps/mobile-app/.eslintrc.js
+++ b/apps/mobile-app/.eslintrc.js
@@ -42,6 +42,9 @@ module.exports = {
version: "detect",
},
"import/ignore": ["node_modules/react-native/index\\.js"],
+ 'react-native/components': {
+ Text: ['ThemedText'],
+ },
},
rules: {
// TypeScript
@@ -144,5 +147,12 @@ module.exports = {
"no-console": ["error", { allow: ["warn", "error", "info", "debug"] }],
"spaced-comment": ["error", "always"],
"multiline-comment-style": ["error", "starred-block"],
+
+ // TODO: this line is added to prevent "Raw text (×) cannot be used outside of a tag" errors.
+ // When adding proper i18n multilingual enforcement checks, the following line should be removed
+ 'react-native/no-raw-text': 'off',
+
+ // Disable prop-types rule because we're using TypeScript for type-checking
+ 'react/prop-types': 'off',
},
};
diff --git a/apps/mobile-app/app/(tabs)/_layout.tsx b/apps/mobile-app/app/(tabs)/_layout.tsx
index 17de12bde..782c44015 100644
--- a/apps/mobile-app/app/(tabs)/_layout.tsx
+++ b/apps/mobile-app/app/(tabs)/_layout.tsx
@@ -1,7 +1,7 @@
-import { Tabs, usePathname } from 'expo-router';
+import { Tabs, router } from 'expo-router';
import React, { useEffect } from 'react';
-import { Platform, View } from 'react-native';
-import { router } from 'expo-router';
+import { Platform, StyleSheet, View } from 'react-native';
+
import { IconSymbol } from '@/components/ui/IconSymbol';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { useColors } from '@/hooks/useColorScheme';
@@ -10,7 +10,10 @@ import { useDb } from '@/context/DbContext';
import emitter from '@/utils/EventEmitter';
import { ThemedText } from '@/components/ThemedText';
-export default function TabLayout() {
+/**
+ * This is the main layout for the app. It is used to navigate between the tabs.
+ */
+export default function TabLayout() : React.ReactNode {
const colors = useColors();
const authContext = useAuth();
const dbContext = useDb();
@@ -27,7 +30,7 @@ export default function TabLayout() {
const timer = setTimeout(() => {
router.replace('/login');
}, 0);
- return () => clearTimeout(timer);
+ return () : void => clearTimeout(timer);
}
}, [requireLoginOrUnlock]);
@@ -35,12 +38,40 @@ export default function TabLayout() {
return null;
}
+ const styles = StyleSheet.create({
+ iconContainer: {
+ position: 'relative',
+ },
+ iconNotificationContainer: {
+ alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 8,
+ height: 16,
+ justifyContent: 'center',
+ position: 'absolute',
+ right: -4,
+ top: -4,
+ width: 16,
+ },
+ iconNotificationText: {
+ color: colors.primarySurfaceText,
+ fontSize: 10,
+ fontWeight: '600',
+ lineHeight: 16,
+ textAlign: 'center',
+ },
+ });
+
return (
{
const targetPathname = (e.target as string).split('-')[0];
- console.log('Tab pressed in layout, navigating to:', targetPathname);
emitter.emit('tabPress', targetPathname);
},
}}
@@ -51,7 +82,7 @@ export default function TabLayout() {
tabBarStyle: Platform.select({
ios: {
position: 'absolute',
- //backgroundColor: colors.tabBarBackground,
+ // backgroundColor: colors.tabBarBackground,
},
default: {},
}),
@@ -60,6 +91,9 @@ export default function TabLayout() {
name="credentials"
options={{
title: 'Credentials',
+ /**
+ * Icon for the credentials tab.
+ */
tabBarIcon: ({ color }) => ,
}}
/>
@@ -67,6 +101,9 @@ export default function TabLayout() {
name="emails"
options={{
title: 'Emails',
+ /**
+ * Icon for the emails tab.
+ */
tabBarIcon: ({ color }) => ,
}}
/>
@@ -74,34 +111,15 @@ export default function TabLayout() {
name="settings"
options={{
title: 'Settings',
+ /**
+ * Icon for the settings tab.
+ */
tabBarIcon: ({ color }) => (
-
+
{Platform.OS === 'ios' && authContext.shouldShowIosAutofillReminder && (
-
-
- 1
-
+
+ 1
)}
diff --git a/apps/mobile-app/app/(tabs)/credentials/[id].tsx b/apps/mobile-app/app/(tabs)/credentials/[id].tsx
index cc323da1f..b5ee78ab2 100644
--- a/apps/mobile-app/app/(tabs)/credentials/[id].tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/[id].tsx
@@ -1,7 +1,9 @@
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking } from 'react-native';
import Toast from 'react-native-toast-message';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedScrollView } from '@/components/ThemedScrollView';
@@ -13,11 +15,13 @@ import { AliasDetails } from '@/components/credentialDetails/AliasDetails';
import { NotesSection } from '@/components/credentialDetails/NotesSection';
import { EmailPreview } from '@/components/credentialDetails/EmailPreview';
import { TotpSection } from '@/components/credentialDetails/TotpSection';
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useColors } from '@/hooks/useColorScheme';
import emitter from '@/utils/EventEmitter';
-export default function CredentialDetailsScreen() {
+/**
+ * Credential details screen.
+ */
+export default function CredentialDetailsScreen() : React.ReactNode {
const { id } = useLocalSearchParams();
const [credential, setCredential] = useState(null);
const [isLoading, setIsLoading] = useState(true);
@@ -26,33 +30,44 @@ export default function CredentialDetailsScreen() {
const colors = useColors();
const router = useRouter();
+ /**
+ * Handle the edit button press.
+ */
+ const handleEdit = useCallback(() : void => {
+ router.push(`/(tabs)/credentials/add-edit?id=${id}`);
+ }, [id, router]);
+
// Set header buttons
useEffect(() => {
navigation.setOptions({
+ /**
+ * Header right button.
+ */
headerRight: () => (
-
+
+ name="edit"
+ size={24}
+ color={colors.primary}
+ />
),
});
- }, [navigation, credential]);
-
- const handleEdit = () => {
- router.push(`/(tabs)/credentials/add-edit?id=${id}`);
- }
+ }, [navigation, credential, handleEdit, colors.primary]);
useEffect(() => {
- const loadCredential = async () => {
- if (!dbContext.dbAvailable || !id) return;
+ /**
+ * Load the credential.
+ */
+ const loadCredential = async () : Promise => {
+ if (!dbContext.dbAvailable || !id) {
+ return;
+ }
try {
const cred = await dbContext.sqliteClient!.getCredentialById(id as string);
@@ -69,20 +84,19 @@ export default function CredentialDetailsScreen() {
// Add listener for credential changes
const credentialChangedSub = emitter.addListener('credentialChanged', async (changedId: string) => {
if (changedId === id) {
- console.log('This credential was changed, refreshing details');
await loadCredential();
}
});
- return () => {
+ return () : void => {
credentialChangedSub.remove();
Toast.hide();
};
- }, [id, dbContext.dbAvailable]);
+ }, [id, dbContext.dbAvailable, dbContext.sqliteClient]);
if (isLoading) {
return (
-
+
);
@@ -94,8 +108,8 @@ export default function CredentialDetailsScreen() {
return (
@@ -125,20 +139,34 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
+ contentContainer: {
+ paddingBottom: 40,
+ },
header: {
- flexDirection: 'row',
alignItems: 'center',
- padding: 16,
+ flexDirection: 'row',
gap: 12,
marginTop: 6,
+ padding: 16,
+ },
+ headerRightButton: {
+ padding: 10,
+ },
+ headerRightContainer: {
+ flexDirection: 'row',
},
headerText: {
flex: 1,
},
+ loadingContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
logo: {
- width: 48,
- height: 48,
borderRadius: 8,
+ height: 48,
+ width: 48,
},
serviceName: {
fontSize: 24,
diff --git a/apps/mobile-app/app/(tabs)/credentials/_layout.tsx b/apps/mobile-app/app/(tabs)/credentials/_layout.tsx
index 7ad4eae94..2900c79db 100644
--- a/apps/mobile-app/app/(tabs)/credentials/_layout.tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/_layout.tsx
@@ -1,8 +1,12 @@
-import { useColors } from '@/hooks/useColorScheme';
import { Stack } from 'expo-router';
import { Platform } from 'react-native';
-export default function CredentialsLayout() {
+import { useColors } from '@/hooks/useColorScheme';
+
+/**
+ * Credentials layout.
+ */
+export default function CredentialsLayout() : React.ReactNode {
const colors = useColors();
return (
diff --git a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
index 4e35667ec..bc9342b9e 100644
--- a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx
@@ -1,6 +1,11 @@
-import { StyleSheet, View, TextInput, TouchableOpacity, ScrollView, ActivityIndicator, Alert, Keyboard } from 'react-native';
-import { useState, useEffect, useRef } from 'react';
+import { StyleSheet, View, TouchableOpacity, ScrollView, Alert, Keyboard } from 'react-native';
+import { useState, useEffect, useRef, useCallback } from 'react';
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import Toast from 'react-native-toast-message';
+import { Resolver, useForm } from 'react-hook-form';
+import { yupResolver } from '@hookform/resolvers/yup';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
@@ -8,8 +13,6 @@ import { useColors } from '@/hooks/useColorScheme';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
import { Credential } from '@/utils/types/Credential';
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
-import Toast from 'react-native-toast-message';
import emitter from '@/utils/EventEmitter';
import { FaviconExtractModel } from '@/utils/types/webapi/FaviconExtractModel';
import { AliasVaultToast } from '@/components/Toast';
@@ -17,14 +20,15 @@ import { useVaultMutate } from '@/hooks/useVaultMutate';
import { IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, BaseIdentityGenerator } from '@/utils/shared/identity-generator';
import { PasswordGenerator } from '@/utils/shared/password-generator';
import { ValidatedFormField, ValidatedFormFieldRef } from '@/components/ValidatedFormField';
-import { Resolver, useForm } from 'react-hook-form';
-import { yupResolver } from '@hookform/resolvers/yup';
import { credentialSchema } from '@/utils/validationSchema';
import LoadingOverlay from '@/components/LoadingOverlay';
type CredentialMode = 'random' | 'manual';
-export default function AddEditCredentialScreen() {
+/**
+ * Add or edit a credential screen.
+ */
+export default function AddEditCredentialScreen() : React.ReactNode {
const { id, serviceUrl } = useLocalSearchParams<{ id: string, serviceUrl?: string }>();
const router = useRouter();
const colors = useColors();
@@ -36,7 +40,7 @@ export default function AddEditCredentialScreen() {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const serviceNameRef = useRef(null);
- const { control, handleSubmit, formState: { errors }, setValue, watch } = useForm({
+ const { control, handleSubmit, setValue, watch } = useForm({
resolver: yupResolver(credentialSchema) as Resolver,
defaultValues: {
Id: "",
@@ -61,31 +65,10 @@ export default function AddEditCredentialScreen() {
*/
const isEditMode = id !== undefined && id.length > 0;
- /**
- * On mount, load an existing credential if we're in edit mode, or extract the service name from the service URL
- * if we're in add mode and the service URL is provided (by native autofill component).
- */
- useEffect(() => {
- if (isEditMode) {
- loadExistingCredential();
- } else if (serviceUrl) {
- const decodedUrl = decodeURIComponent(serviceUrl);
- const serviceName = extractServiceNameFromUrl(decodedUrl);
- setValue('ServiceUrl', decodedUrl);
- setValue('ServiceName', serviceName);
-
- // Focus and select the service name field
- setTimeout(() => {
- serviceNameRef.current?.focus();
- serviceNameRef.current?.selectAll();
- }, 100);
- }
- }, [id, isEditMode, serviceUrl]);
-
/**
* Load an existing credential from the database in edit mode.
*/
- const loadExistingCredential = async () => {
+ const loadExistingCredential = useCallback(async () : Promise => {
try {
const existingCredential = await dbContext.sqliteClient!.getCredentialById(id);
if (existingCredential) {
@@ -105,13 +88,99 @@ export default function AddEditCredentialScreen() {
text2: 'Please try again'
});
}
- };
+ }, [id, dbContext.sqliteClient, setValue]);
+
+ /**
+ * On mount, load an existing credential if we're in edit mode, or extract the service name from the service URL
+ * if we're in add mode and the service URL is provided (by native autofill component).
+ */
+ useEffect(() => {
+ if (isEditMode) {
+ loadExistingCredential();
+ } else if (serviceUrl) {
+ const decodedUrl = decodeURIComponent(serviceUrl);
+ const serviceName = extractServiceNameFromUrl(decodedUrl);
+ setValue('ServiceUrl', decodedUrl);
+ setValue('ServiceName', serviceName);
+
+ // Focus and select the service name field
+ setTimeout(() => {
+ serviceNameRef.current?.focus();
+ serviceNameRef.current?.selectAll();
+ }, 100);
+ }
+ }, [id, isEditMode, serviceUrl, loadExistingCredential, setValue]);
+
+ /**
+ * Initialize the identity and password generators with settings from user's vault.
+ * @returns {identityGenerator: BaseIdentityGenerator, passwordGenerator: PasswordGenerator}
+ */
+ const initializeGenerators = useCallback(async () : Promise<{ identityGenerator: BaseIdentityGenerator, passwordGenerator: PasswordGenerator }> => {
+ // Get default identity language from database
+ const identityLanguage = await dbContext.sqliteClient!.getDefaultIdentityLanguage();
+
+ // Initialize identity generator based on language
+ let identityGenerator: BaseIdentityGenerator;
+ switch (identityLanguage) {
+ case 'nl':
+ identityGenerator = new IdentityGeneratorNl();
+ break;
+ case 'en':
+ default:
+ identityGenerator = new IdentityGeneratorEn();
+ break;
+ }
+
+ // Get password settings from database
+ const passwordSettings = await dbContext.sqliteClient!.getPasswordSettings();
+
+ // Initialize password generator with settings
+ const passwordGenerator = new PasswordGenerator(passwordSettings);
+
+ return { identityGenerator, passwordGenerator };
+ }, [dbContext.sqliteClient]);
+
+ /**
+ * Generate a random alias and password.
+ */
+ const generateRandomAlias = useCallback(async (): Promise => {
+ const { identityGenerator, passwordGenerator } = await initializeGenerators();
+
+ const identity = await identityGenerator.generateRandomIdentity();
+ const password = passwordGenerator.generateRandomPassword();
+ const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
+ const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
+
+ setValue('Alias.Email', email);
+ setValue('Alias.FirstName', identity.firstName);
+ setValue('Alias.LastName', identity.lastName);
+ setValue('Alias.NickName', identity.nickName);
+ setValue('Alias.Gender', identity.gender);
+ setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
+
+ // In edit mode, preserve existing username and password if they exist
+ if (isEditMode && watch('Username')) {
+ // Keep the existing username in edit mode, so don't do anything here.
+ } else {
+ // Use the newly generated username
+ setValue('Username', identity.nickName);
+ }
+
+ if (isEditMode && watch('Password')) {
+ // Keep the existing password in edit mode, so don't do anything here.
+ } else {
+ // Use the newly generated password
+ setValue('Password', password);
+ // Make password visible when newly generated
+ setIsPasswordVisible(true);
+ }
+ }, [isEditMode, watch, setValue, setIsPasswordVisible, initializeGenerators, dbContext.sqliteClient]);
/**
* Submit the form for either creating or updating a credential.
* @param {Credential} data - The form data.
*/
- const onSubmit = async (data: Credential) => {
+ const onSubmit = useCallback(async (data: Credential) : Promise => {
Keyboard.dismiss();
// If we're creating a new credential and mode is random, generate random values
@@ -120,19 +189,19 @@ export default function AddEditCredentialScreen() {
}
// Assemble the credential to save
- let credentialToSave: Credential = {
+ const credentialToSave: Credential = {
Id: isEditMode ? id : '',
- Username: watch('Username'),
- Password: watch('Password'),
- ServiceName: watch('ServiceName'),
- ServiceUrl: watch('ServiceUrl'),
+ Username: data.Username,
+ Password: data.Password,
+ ServiceName: data.ServiceName,
+ ServiceUrl: data.ServiceUrl,
Alias: {
- FirstName: watch('Alias.FirstName'),
- LastName: watch('Alias.LastName'),
- NickName: watch('Alias.NickName'),
- BirthDate: watch('Alias.BirthDate'),
- Gender: watch('Alias.Gender'),
- Email: watch('Alias.Email')
+ FirstName: data.Alias.FirstName,
+ LastName: data.Alias.LastName,
+ NickName: data.Alias.NickName,
+ BirthDate: data.Alias.BirthDate,
+ Gender: data.Alias.Gender,
+ Email: data.Alias.Email
}
}
@@ -152,8 +221,8 @@ export default function AddEditCredentialScreen() {
const decodedImage = Uint8Array.from(Buffer.from(faviconResponse.image as string, 'base64'));
credentialToSave.Logo = decodedImage;
}
- } catch (error) {
- console.log('Favicon extraction failed or timed out:', error);
+ } catch {
+ // Favicon extraction failed or timed out, this is not a critical error so we can ignore it.
}
}
@@ -190,8 +259,11 @@ export default function AddEditCredentialScreen() {
});
}, 200);
}
- };
+ }, [isEditMode, id, serviceUrl, router, executeVaultMutation, dbContext.sqliteClient, mode, generateRandomAlias, webApi]);
+ /**
+ * Extract the service name from the service URL.
+ */
function extractServiceNameFromUrl(url: string): string {
try {
const urlObj = new URL(url);
@@ -212,100 +284,16 @@ export default function AddEditCredentialScreen() {
// For domains like app.example.com, return Example.com
const mainDomain = hostParts.slice(-2).join('.');
return mainDomain.charAt(0).toUpperCase() + mainDomain.slice(1);
- } catch (e) {
+ } catch {
// If URL parsing fails, return the original URL
return url;
}
}
- // Set header buttons
- useEffect(() => {
- navigation.setOptions({
- title: isEditMode ? 'Edit Credential' : 'Add Credential',
- headerLeft: () => (
- router.back()}
- style={{ padding: 10, paddingLeft: 0 }}
- >
- Cancel
-
- ),
- headerRight: () => (
-
-
-
-
-
- ),
- });
- }, [navigation, mode]);
-
/**
- * Initialize the identity and password generators with settings from user's vault.
- * @returns {identityGenerator: BaseIdentityGenerator, passwordGenerator: PasswordGenerator}
+ * Generate a random username.
*/
- const initializeGenerators = async () => {
- // Get default identity language from database
- const identityLanguage = await dbContext.sqliteClient!.getDefaultIdentityLanguage();
-
- // Initialize identity generator based on language
- let identityGenerator: BaseIdentityGenerator;
- switch (identityLanguage) {
- case 'nl':
- identityGenerator = new IdentityGeneratorNl();
- break;
- case 'en':
- default:
- identityGenerator = new IdentityGeneratorEn();
- break;
- }
-
- // Get password settings from database
- const passwordSettings = await dbContext.sqliteClient!.getPasswordSettings();
-
- // Initialize password generator with settings
- const passwordGenerator = new PasswordGenerator(passwordSettings);
-
- return { identityGenerator, passwordGenerator };
- };
-
- const generateRandomAlias = async (): Promise => {
- const { identityGenerator, passwordGenerator } = await initializeGenerators();
-
- const identity = await identityGenerator.generateRandomIdentity();
- const password = passwordGenerator.generateRandomPassword();
- const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
- const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
-
- setValue('Alias.Email', email);
- setValue('Alias.FirstName', identity.firstName);
- setValue('Alias.LastName', identity.lastName);
- setValue('Alias.NickName', identity.nickName);
- setValue('Alias.Gender', identity.gender);
- setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
-
- // In edit mode, preserve existing username and password if they exist
- if (isEditMode && watch('Username')) {
- // Keep the existing username in edit mode, so don't do anything here.
- } else {
- // Use the newly generated username
- setValue('Username', identity.nickName);
- }
-
- if (isEditMode && watch('Password')) {
- // Keep the existing password in edit mode, so don't do anything here.
- } else {
- // Use the newly generated password
- setValue('Password', password);
- // Make password visible when newly generated
- setIsPasswordVisible(true);
- }
- };
-
- const generateRandomUsername = async () => {
+ const generateRandomUsername = async () : Promise => {
try {
const { identityGenerator } = await initializeGenerators();
const identity = await identityGenerator.generateRandomIdentity();
@@ -320,7 +308,10 @@ export default function AddEditCredentialScreen() {
}
};
- const generateRandomPassword = async () => {
+ /**
+ * Generate a random password.
+ */
+ const generateRandomPassword = async () : Promise => {
try {
const { passwordGenerator } = await initializeGenerators();
const password = passwordGenerator.generateRandomPassword();
@@ -336,7 +327,10 @@ export default function AddEditCredentialScreen() {
}
};
- const handleDelete = async () => {
+ /**
+ * Handle the delete button press.
+ */
+ const handleDelete = async () : Promise => {
if (!id) {
return;
}
@@ -354,12 +348,12 @@ export default function AddEditCredentialScreen() {
{
text: "Delete",
style: "destructive",
- onPress: async () => {
-
+ /**
+ * Delete the credential.
+ */
+ onPress: async () : Promise => {
await executeVaultMutation(async () => {
- console.log('Starting delete operation');
await dbContext.sqliteClient!.deleteCredentialById(id);
- console.log('Credential deleted successfully');
});
// Show success toast
@@ -371,8 +365,10 @@ export default function AddEditCredentialScreen() {
});
}, 200);
- // Hard navigate back to the credentials list as the credential that was
- // shown in the previous screen is now deleted.
+ /*
+ * Hard navigate back to the credentials list as the credential that was
+ * shown in the previous screen is now deleted.
+ */
router.replace('/credentials');
}
}
@@ -386,22 +382,50 @@ export default function AddEditCredentialScreen() {
},
content: {
flex: 1,
+ marginTop: 36,
padding: 16,
paddingTop: 0,
- marginTop: 36,
},
- modeSelector: {
- flexDirection: 'row',
- marginBottom: 16,
- backgroundColor: colors.accentBackground,
+ deleteButton: {
+ alignItems: 'center',
+ backgroundColor: colors.errorBackground,
+ borderColor: colors.errorBorder,
borderRadius: 8,
- padding: 4,
+ borderWidth: 1,
+ padding: 10,
+ },
+ deleteButtonText: {
+ color: colors.errorText,
+ fontWeight: '600',
+ },
+ generateButton: {
+ alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 8,
+ flexDirection: 'row',
+ marginBottom: 8,
+ marginTop: 16,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ },
+ generateButtonText: {
+ color: colors.primarySurfaceText,
+ fontWeight: '600',
+ marginLeft: 6,
+ },
+ headerLeftButton: {
+ padding: 10,
+ paddingLeft: 0,
+ },
+ headerRightButton: {
+ padding: 10,
+ paddingRight: 0,
},
modeButton: {
- flex: 1,
- padding: 12,
alignItems: 'center',
borderRadius: 6,
+ flex: 1,
+ padding: 12,
},
modeButtonActive: {
backgroundColor: colors.primary,
@@ -411,49 +435,58 @@ export default function AddEditCredentialScreen() {
fontWeight: '600',
},
modeButtonTextActive: {
- color: '#fff',
+ color: colors.primarySurfaceText,
},
- section: {
- marginBottom: 24,
+ modeSelector: {
backgroundColor: colors.accentBackground,
borderRadius: 8,
+ flexDirection: 'row',
+ marginBottom: 16,
+ padding: 4,
+ },
+ section: {
+ backgroundColor: colors.accentBackground,
+ borderRadius: 8,
+ marginBottom: 24,
padding: 16,
},
sectionTitle: {
+ color: colors.text,
fontSize: 18,
fontWeight: '600',
marginBottom: 16,
- color: colors.text,
- },
- generateButton: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: colors.primary,
- paddingVertical: 8,
- paddingHorizontal: 12,
- borderRadius: 8,
- marginBottom: 8,
- marginTop: 16,
- },
- generateButtonText: {
- color: '#fff',
- fontWeight: '600',
- marginLeft: 6,
- },
- deleteButton: {
- padding: 10,
- borderRadius: 8,
- alignItems: 'center',
- backgroundColor: colors.errorBackground,
- borderWidth: 1,
- borderColor: colors.errorBorder,
- },
- deleteButtonText: {
- color: colors.errorText,
- fontWeight: '600',
},
});
+ // Set header buttons
+ useEffect(() => {
+ navigation.setOptions({
+ title: isEditMode ? 'Edit Credential' : 'Add Credential',
+ /**
+ * Header left button.
+ */
+ headerLeft: () => (
+ router.back()}
+ style={styles.headerLeftButton}
+ >
+ Cancel
+
+ ),
+ /**
+ * Header right button.
+ */
+ headerRight: () => (
+
+
+
+ ),
+ });
+ }, [navigation, mode, handleSubmit, onSubmit, colors.primary, isEditMode, router, styles.headerLeftButton, styles.headerRightButton]);
+
return (
<>
{(isLoading) && (
@@ -500,100 +533,103 @@ export default function AddEditCredentialScreen() {
{(mode === 'manual' || isEditMode) && (
<>
-
- Login credentials
+
+ Login credentials
-
- setIsPasswordVisible(!isPasswordVisible)
- },
- {
- icon: "refresh",
- onPress: generateRandomPassword
- }
- ]}
- />
-
-
- Generate Random Alias
-
-
-
+
+ setIsPasswordVisible(!isPasswordVisible)
+ },
+ {
+ icon: "refresh",
+ onPress: generateRandomPassword
+ }
+ ]}
+ />
+
+
+ Generate Random Alias
+
+
+
-
- Alias
-
-
-
-
-
-
+
+ Alias
+
+
+
+
+
+
-
- Metadata
+
+ Metadata
-
- {/* TODO: Add TOTP management */}
-
+
+ {/* TODO: Add TOTP management */}
+
- {isEditMode && (
-
- Delete Credential
-
- )}
- >
+ {isEditMode && (
+
+ Delete Credential
+
+ )}
+ >
)}
diff --git a/apps/mobile-app/app/(tabs)/credentials/autofill-credential-created.tsx b/apps/mobile-app/app/(tabs)/credentials/autofill-credential-created.tsx
index c4a92c246..c629e80cd 100644
--- a/apps/mobile-app/app/(tabs)/credentials/autofill-credential-created.tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/autofill-credential-created.tsx
@@ -1,64 +1,80 @@
-import { StyleSheet, View, TouchableOpacity, Platform } from 'react-native';
+import { StyleSheet, View, TouchableOpacity } from 'react-native';
import { useNavigation, useRouter } from 'expo-router';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import { useCallback, useEffect } from 'react';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
import { useColors } from '@/hooks/useColorScheme';
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
-import { useEffect } from 'react';
-export default function AutofillCredentialCreatedScreen() {
+/**
+ * Autofill credential created screen.
+ */
+export default function AutofillCredentialCreatedScreen() : React.ReactNode {
const router = useRouter();
const colors = useColors();
const navigation = useNavigation();
- // Set header buttons
- useEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
-
- Dismiss
-
-
- ),
- });
- }, [navigation]);
-
- const handleStayInApp = () => {
+ /**
+ * Handle the stay in app button press.
+ */
+ const handleStayInApp = useCallback(() => {
router.back();
- };
+ }, [router]);
const styles = StyleSheet.create({
+ boldMessage: {
+ fontWeight: 'bold',
+ marginTop: 20,
+ },
container: {
flex: 1,
},
content: {
- flex: 1,
- padding: 20,
alignItems: 'center',
+ flex: 1,
justifyContent: 'center',
+ padding: 20,
+ },
+ headerRightButton: {
+ padding: 10,
+ paddingRight: 0,
},
iconContainer: {
marginBottom: 30,
},
+ message: {
+ fontSize: 16,
+ lineHeight: 24,
+ marginBottom: 30,
+ textAlign: 'center',
+ },
title: {
fontSize: 24,
fontWeight: 'bold',
- textAlign: 'center',
marginBottom: 20,
- },
- message: {
- fontSize: 16,
textAlign: 'center',
- marginBottom: 30,
- lineHeight: 24,
},
});
+ // Set header buttons
+ useEffect(() => {
+ navigation.setOptions({
+ /**
+ * Header right button.
+ */
+ headerRight: () => (
+
+ Dismiss
+
+ ),
+ });
+ }, [navigation, colors.primary, styles.headerRightButton, handleStayInApp]);
+
return (
@@ -75,7 +91,7 @@ export default function AutofillCredentialCreatedScreen() {
Your new credential has been added to your vault and is now available for password autofill.
-
+
Switch back to your browser to continue.
diff --git a/apps/mobile-app/app/(tabs)/credentials/email/[id].tsx b/apps/mobile-app/app/(tabs)/credentials/email/[id].tsx
index d52947b4c..ed69ee392 100644
--- a/apps/mobile-app/app/(tabs)/credentials/email/[id].tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/email/[id].tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import EmailDetailsScreen from '../../emails/[id]';
+
+import EmailDetailsScreen from '@/app/(tabs)/emails/[id]';
/**
* CredentialEmailPreviewScreen Component
@@ -13,6 +14,6 @@ import EmailDetailsScreen from '../../emails/[id]';
* - Maintains UI consistency by reusing the same email details view
* - Provides a better user experience by keeping context within the credentials flow
*/
-export default function CredentialEmailPreviewScreen() {
+export default function CredentialEmailPreviewScreen() : React.ReactNode {
return ;
}
diff --git a/apps/mobile-app/app/(tabs)/credentials/index.tsx b/apps/mobile-app/app/(tabs)/credentials/index.tsx
index df6d97eca..e0bd0d573 100644
--- a/apps/mobile-app/app/(tabs)/credentials/index.tsx
+++ b/apps/mobile-app/app/(tabs)/credentials/index.tsx
@@ -2,6 +2,9 @@ import { StyleSheet, Text, FlatList, ActivityIndicator, TouchableOpacity, TextIn
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useRouter } from 'expo-router';
+import Toast from 'react-native-toast-message';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
@@ -14,10 +17,11 @@ import { CredentialCard } from '@/components/CredentialCard';
import { TitleContainer } from '@/components/TitleContainer';
import { CollapsibleHeader } from '@/components/CollapsibleHeader';
import emitter from '@/utils/EventEmitter';
-import Toast from 'react-native-toast-message';
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
-export default function CredentialsScreen() {
+/**
+ * Credentials screen.
+ */
+export default function CredentialsScreen() : React.ReactNode {
const [searchQuery, setSearchQuery] = useState('');
const [refreshing, setRefreshing] = useState(false);
const { syncVault } = useVaultSync();
@@ -27,11 +31,39 @@ export default function CredentialsScreen() {
const navigation = useNavigation();
const [isTabFocused, setIsTabFocused] = useState(false);
const router = useRouter();
+ const [credentialsList, setCredentialsList] = useState([]);
+ const [isLoadingCredentials, setIsLoadingCredentials] = useState(false);
+
+ const authContext = useAuth();
+ const dbContext = useDb();
+
+ const isAuthenticated = authContext.isLoggedIn;
+ const isDatabaseAvailable = dbContext.dbAvailable;
+
+ /**
+ * Load credentials.
+ */
+ const loadCredentials = useCallback(async () : Promise => {
+ try {
+ const credentials = await dbContext.sqliteClient!.getAllCredentials();
+ setCredentialsList(credentials);
+ } catch (err) {
+ // Error loading credentials, show error toast
+ Toast.show({
+ type: 'error',
+ text1: 'Error loading credentials',
+ text2: err instanceof Error ? err.message : 'Unknown error',
+ });
+ }
+ }, [dbContext.sqliteClient]);
const headerButtons = [{
icon: 'add' as const,
position: 'right' as const,
- onPress: () => router.push('/(tabs)/credentials/add-edit')
+ /**
+ * Add credential.
+ */
+ onPress: () : void => router.push('/(tabs)/credentials/add-edit')
}];
useEffect(() => {
@@ -45,7 +77,6 @@ export default function CredentialsScreen() {
const tabPressSub = emitter.addListener('tabPress', (routeName: string) => {
if (routeName === 'credentials' && isTabFocused) {
- console.log('Credentials tab re-pressed while focused: reset screen');
setSearchQuery(''); // Reset search
setRefreshing(false); // Reset refreshing
// Scroll to top
@@ -55,35 +86,16 @@ export default function CredentialsScreen() {
// Add listener for credential changes
const credentialChangedSub = emitter.addListener('credentialChanged', async () => {
- console.log('Credential changed, refreshing list');
await loadCredentials();
});
- return () => {
+ return () : void => {
tabPressSub.remove();
credentialChangedSub.remove();
unsubscribeFocus();
unsubscribeBlur();
};
- }, [isTabFocused]);
-
- const [credentialsList, setCredentialsList] = useState([]);
- const [isLoadingCredentials, setIsLoadingCredentials] = useState(false);
-
- const authContext = useAuth();
- const dbContext = useDb();
-
- const isAuthenticated = authContext.isLoggedIn;
- const isDatabaseAvailable = dbContext.dbAvailable;
-
- const loadCredentials = async () => {
- try {
- const credentials = await dbContext.sqliteClient!.getAllCredentials();
- setCredentialsList(credentials);
- } catch (err) {
- console.error('Error loading credentials:', err);
- }
- };
+ }, [isTabFocused, loadCredentials, navigation]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
@@ -94,6 +106,9 @@ export default function CredentialsScreen() {
// Sync vault and load credentials
await syncVault({
+ /**
+ * On success.
+ */
onSuccess: async (hasNewVault) => {
// Calculate remaining time needed to reach minimum duration
const elapsedTime = Date.now() - startTime;
@@ -113,6 +128,9 @@ export default function CredentialsScreen() {
visibilityTime: 1200,
});
},
+ /**
+ * On error.
+ */
onError: (error) => {
console.error('Error syncing vault:', error);
setRefreshing(false);
@@ -142,70 +160,74 @@ export default function CredentialsScreen() {
setIsLoadingCredentials(true);
loadCredentials();
setIsLoadingCredentials(false);
- }, [isAuthenticated, isDatabaseAvailable]);
+ }, [isAuthenticated, isDatabaseAvailable, loadCredentials]);
const filteredCredentials = credentialsList.filter(credential => {
const searchLower = searchQuery.toLowerCase();
+
return (
- credential.ServiceName?.toLowerCase().includes(searchLower) ||
- credential.Username?.toLowerCase().includes(searchLower) ||
- credential.Alias?.Email?.toLowerCase().includes(searchLower) ||
+ credential.ServiceName?.toLowerCase().includes(searchLower) ??
+ credential.Username?.toLowerCase().includes(searchLower) ??
+ credential.Alias?.Email?.toLowerCase().includes(searchLower) ??
credential.ServiceUrl?.toLowerCase().includes(searchLower)
);
});
const styles = StyleSheet.create({
+ clearButton: {
+ padding: 4,
+ position: 'absolute',
+ right: 8,
+ top: '50%',
+ transform: [{ translateY: -12 }],
+ },
+ clearButtonText: {
+ color: colors.textMuted,
+ fontSize: 20,
+ },
container: {
flex: 1,
},
content: {
flex: 1,
+ marginTop: 36,
padding: 16,
paddingTop: 0,
- marginTop: 36,
+ },
+ contentContainer: {
+ paddingBottom: 40,
+ paddingTop: 4,
+ },
+ emptyText: {
+ color: colors.textMuted,
+ fontSize: 16,
+ marginTop: 24,
+ textAlign: 'center',
+ },
+ searchContainer: {
+ position: 'relative',
+ },
+ searchIcon: {
+ left: 12,
+ position: 'absolute',
+ top: '50%',
+ transform: [{ translateY: -17 }],
+ zIndex: 1,
+ },
+ searchInput: {
+ backgroundColor: colors.accentBackground,
+ borderRadius: 8,
+ color: colors.text,
+ fontSize: 16,
+ marginBottom: 16,
+ padding: 12,
+ paddingLeft: 40,
+ paddingRight: Platform.OS === 'android' ? 40 : 12,
},
stepContainer: {
flex: 1,
gap: 8,
},
- credentialItem: {
- backgroundColor: colors.accentBackground,
- borderColor: colors.accentBorder,
- padding: 12,
- borderRadius: 8,
- marginBottom: 8,
- borderWidth: 1,
- },
- emptyText: {
- color: colors.textMuted,
- textAlign: 'center',
- fontSize: 16,
- marginTop: 24,
- },
- searchInput: {
- backgroundColor: colors.accentBackground,
- color: colors.text,
- padding: 12,
- borderRadius: 8,
- marginBottom: 16,
- fontSize: 16,
- paddingRight: Platform.OS === 'android' ? 40 : 12,
- paddingLeft: 40,
- },
- clearButton: {
- position: 'absolute',
- right: 8,
- top: '50%',
- transform: [{ translateY: -12 }],
- padding: 4,
- },
- searchIcon: {
- position: 'absolute',
- left: 12,
- top: '50%',
- transform: [{ translateY: -17 }],
- zIndex: 1,
- },
});
return (
@@ -233,12 +255,12 @@ export default function CredentialsScreen() {
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
- contentContainerStyle={{ paddingBottom: 40, paddingTop: 4 }}
+ contentContainerStyle={styles.contentContainer}
scrollIndicatorInsets={{ bottom: 40 }}
ListHeaderComponent={
-
+
setSearchQuery('')}
>
- ×
+ ×
)}
@@ -278,7 +300,7 @@ export default function CredentialsScreen() {
)}
ListEmptyComponent={
-
+
{searchQuery ? 'No matching credentials found' : 'No credentials found'}
}
diff --git a/apps/mobile-app/app/(tabs)/emails/[id].tsx b/apps/mobile-app/app/(tabs)/emails/[id].tsx
index 02d3902d3..5f4aeca2d 100644
--- a/apps/mobile-app/app/(tabs)/emails/[id].tsx
+++ b/apps/mobile-app/app/(tabs)/emails/[id].tsx
@@ -1,20 +1,24 @@
-import React, { useEffect, useState } from 'react';
-import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, TextInput, Linking } from 'react-native';
+import React, { useEffect, useState, useCallback } from 'react';
+import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, TextInput } from 'react-native';
import { useLocalSearchParams, useRouter, useNavigation, Stack } from 'expo-router';
+import { WebView } from 'react-native-webview';
+import * as FileSystem from 'expo-file-system';
+import { Ionicons } from '@expo/vector-icons';
+
import { Email } from '@/utils/types/webapi/Email';
import { Credential } from '@/utils/types/Credential';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
import { ThemedText } from '@/components/ThemedText';
import EncryptionUtility from '@/utils/EncryptionUtility';
-import WebView from 'react-native-webview';
-import * as FileSystem from 'expo-file-system';
-import { Ionicons } from '@expo/vector-icons';
import { useColors } from '@/hooks/useColorScheme';
import { IconSymbol } from '@/components/ui/IconSymbol';
import emitter from '@/utils/EventEmitter';
-export default function EmailDetailsScreen() {
+/**
+ * Email details screen.
+ */
+export default function EmailDetailsScreen() : React.ReactNode {
const { id } = useLocalSearchParams();
const router = useRouter();
const navigation = useNavigation();
@@ -29,37 +33,10 @@ export default function EmailDetailsScreen() {
const isDarkMode = useColorScheme() === 'dark';
const [associatedCredential, setAssociatedCredential] = useState(null);
- useEffect(() => {
- loadEmail();
- }, [id]);
-
- // Set navigation options
- useEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
- setHtmlView(!isHtmlView)}
- style={{ padding: 10, paddingRight: 0 }}
- >
-
-
-
-
-
-
- ),
- });
- }, [isHtmlView, navigation]);
-
- const loadEmail = async () => {
+ /**
+ * Load the email.
+ */
+ const loadEmail = useCallback(async () : Promise => {
try {
setIsLoading(true);
setError(null);
@@ -93,9 +70,16 @@ export default function EmailDetailsScreen() {
} finally {
setIsLoading(false);
}
- };
+ }, [dbContext.sqliteClient, id, webApi]);
- const handleDelete = async () => {
+ useEffect(() => {
+ loadEmail();
+ }, [id, loadEmail]);
+
+ /**
+ * Handle the delete button press.
+ */
+ const handleDelete = useCallback(async () : Promise => {
Alert.alert(
'Delete Email',
'Are you sure you want to delete this email? This action is permanent and cannot be undone.',
@@ -107,7 +91,10 @@ export default function EmailDetailsScreen() {
{
text: 'Delete',
style: 'destructive',
- onPress: async () => {
+ /**
+ * Handle the delete button press.
+ */
+ onPress: async () : Promise => {
try {
// Delete the email from the server.
await webApi.delete(`Email/${id}`);
@@ -124,9 +111,12 @@ export default function EmailDetailsScreen() {
},
]
);
- };
+ }, [id, router, webApi]);
- const handleDownloadAttachment = async (attachment: Email['attachments'][0]) => {
+ /**
+ * Handle the download attachment button press.
+ */
+ const handleDownloadAttachment = async (attachment: Email['attachments'][0]) : Promise => {
try {
const base64EncryptedAttachment = await webApi.downloadBlobAndConvertToBase64(
`Email/${id}/attachments/${attachment.id}`
@@ -166,108 +156,134 @@ export default function EmailDetailsScreen() {
}
};
- const handleOpenCredential = () => {
+ /**
+ * Handle the open credential button press.
+ */
+ const handleOpenCredential = () : void => {
if (associatedCredential) {
router.push(`/(tabs)/credentials/${associatedCredential.Id}`);
}
};
const styles = StyleSheet.create({
+ attachment: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderRadius: 8,
+ flexDirection: 'row',
+ marginBottom: 8,
+ padding: 12,
+ },
+ attachmentName: {
+ color: colors.textMuted,
+ fontSize: 14,
+ marginLeft: 8,
+ },
+ attachments: {
+ borderTopColor: colors.accentBorder,
+ borderTopWidth: 1,
+ padding: 16,
+ },
+ attachmentsTitle: {
+ color: colors.text,
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 12,
+ },
+ centerContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 20,
+ },
container: {
flex: 1,
},
- viewLight: {
- backgroundColor: colors.background,
+ divider: {
+ backgroundColor: colors.accentBorder,
+ height: 1,
+ marginVertical: 2,
},
- viewDark: {
- backgroundColor: colors.background,
- },
- centerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: 20,
+ emptyText: {
+ color: colors.textMuted,
+ opacity: 0.7,
+ textAlign: 'center',
},
errorText: {
color: colors.errorBackground,
textAlign: 'center',
},
- emptyText: {
- textAlign: 'center',
- opacity: 0.7,
- color: colors.textMuted,
+ headerRightButton: {
+ padding: 10,
+ paddingRight: 0,
},
- topBox: {
- backgroundColor: colors.background,
+ headerRightContainer: {
flexDirection: 'row',
- alignSelf: 'flex-start',
- padding: 2,
- },
- subjectContainer: {
- paddingBottom: 8,
- paddingTop: 8,
- paddingLeft: 5,
- width: '90%',
- },
- metadataIcon: {
- width: 30,
- paddingTop: 6,
},
metadataContainer: {
padding: 2,
},
+ metadataCredential: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ metadataCredentialIcon: {
+ marginRight: 4,
+ },
+ metadataHeading: {
+ color: colors.text,
+ fontSize: 13,
+ fontWeight: 'bold',
+ marginBottom: 0,
+ marginTop: 0,
+ paddingBottom: 0,
+ paddingTop: 0,
+ },
+ metadataIcon: {
+ paddingTop: 6,
+ width: 30,
+ },
+ metadataLabel: {
+ paddingBottom: 4,
+ paddingLeft: 5,
+ paddingTop: 4,
+ width: 60,
+ },
metadataRow: {
flexDirection: 'row',
justifyContent: 'flex-start',
padding: 2,
},
- metadataLabel: {
- paddingBottom: 4,
- paddingTop: 4,
- paddingLeft: 5,
- width: 60,
+ metadataText: {
+ color: colors.text,
+ fontSize: 13,
+ marginBottom: 0,
+ marginTop: 0,
+ paddingBottom: 0,
+ paddingTop: 0,
},
metadataValue: {
+ flex: 1,
paddingBottom: 4,
- paddingTop: 4,
paddingLeft: 5,
- flex: 1,
- },
- metadataHeading: {
- fontWeight: 'bold',
- marginTop: 0,
- paddingTop: 0,
- marginBottom: 0,
- paddingBottom: 0,
- fontSize: 13,
- color: colors.text,
- },
- metadataText: {
- marginTop: 0,
- marginBottom: 0,
- paddingTop: 0,
- paddingBottom: 0,
- fontSize: 13,
- color: colors.text,
- },
- divider: {
- height: 1,
- backgroundColor: colors.accentBorder,
- marginVertical: 2,
- },
- subject: {
- fontSize: 14,
- fontWeight: 'bold',
- textAlign: 'center',
- color: colors.text,
- },
- webView: {
- flex: 1,
+ paddingTop: 4,
},
plainText: {
flex: 1,
- padding: 16,
fontSize: 15,
+ padding: 16,
+ },
+ subject: {
+ color: colors.text,
+ fontSize: 14,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ subjectContainer: {
+ paddingBottom: 8,
+ paddingLeft: 5,
+ paddingTop: 8,
+ width: '90%',
},
textDark: {
color: colors.text,
@@ -275,32 +291,52 @@ export default function EmailDetailsScreen() {
textLight: {
color: colors.text,
},
- attachments: {
- padding: 16,
- borderTopWidth: 1,
- borderTopColor: colors.accentBorder,
- },
- attachmentsTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- marginBottom: 12,
- color: colors.text,
- },
- attachment: {
+ topBox: {
+ alignSelf: 'flex-start',
+ backgroundColor: colors.background,
flexDirection: 'row',
- alignItems: 'center',
- padding: 12,
- backgroundColor: colors.accentBackground,
- borderRadius: 8,
- marginBottom: 8,
+ padding: 2,
},
- attachmentName: {
- marginLeft: 8,
- fontSize: 14,
- color: colors.textMuted,
+ viewDark: {
+ backgroundColor: colors.background,
+ },
+ viewLight: {
+ backgroundColor: colors.background,
+ },
+ webView: {
+ flex: 1,
},
});
+ // Set navigation options
+ useEffect(() => {
+ navigation.setOptions({
+ /**
+ * Header right button.
+ */
+ headerRight: () => (
+
+ setHtmlView(!isHtmlView)}
+ style={styles.headerRightButton}
+ >
+
+
+
+
+
+
+ ),
+ });
+ }, [isHtmlView, navigation, handleDelete, styles.headerRightButton, styles.headerRightContainer]);
+
if (isLoading) {
return (
@@ -351,15 +387,15 @@ export default function EmailDetailsScreen() {
{email.subject}
{associatedCredential && (
- <>
-
-
-
- {associatedCredential.ServiceName}
-
-
- >
- )}
+ <>
+
+
+
+ {associatedCredential.ServiceName}
+
+
+ >
+ )}
diff --git a/apps/mobile-app/app/(tabs)/emails/_layout.tsx b/apps/mobile-app/app/(tabs)/emails/_layout.tsx
index 5aa341fcd..2e31bec43 100644
--- a/apps/mobile-app/app/(tabs)/emails/_layout.tsx
+++ b/apps/mobile-app/app/(tabs)/emails/_layout.tsx
@@ -1,7 +1,11 @@
-import { useColors } from '@/hooks/useColorScheme';
import { Stack } from 'expo-router';
-export default function EmailsLayout() {
+import { useColors } from '@/hooks/useColorScheme';
+
+/**
+ * Emails layout.
+ */
+export default function EmailsLayout() : React.ReactNode {
const colors = useColors();
return (
diff --git a/apps/mobile-app/app/(tabs)/emails/index.tsx b/apps/mobile-app/app/(tabs)/emails/index.tsx
index 3e4a14ad5..8436659de 100644
--- a/apps/mobile-app/app/(tabs)/emails/index.tsx
+++ b/apps/mobile-app/app/(tabs)/emails/index.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { StyleSheet, View, ActivityIndicator, ScrollView, RefreshControl, Animated } from 'react-native';
-import { Stack, useNavigation } from 'expo-router';
+import { useNavigation } from 'expo-router';
+
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
@@ -15,31 +16,45 @@ import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
import { EmailCard } from '@/components/EmailCard';
import emitter from '@/utils/EventEmitter';
-// Simple hook for minimum duration loading state
-const useMinDurationLoading = (initialState: boolean, minDuration: number): [boolean, (newState: boolean) => void] => {
+/**
+ * Hook for minimum duration loading state.
+ */
+const useMinDurationLoading = (
+ initialState: boolean,
+ minDuration: number
+): [boolean, (newState: boolean) => void] => {
const [state, setState] = useState(initialState);
- const [timer, setTimer] = useState(null);
+ const timerRef = useRef(null);
- const setStateWithMinDuration = useCallback((newState: boolean) => {
- if (newState) {
- setState(true);
- } else {
- if (timer) clearTimeout(timer);
- const newTimer = setTimeout(() => setState(false), minDuration);
- setTimer(newTimer);
- }
- }, [minDuration, timer]);
+ const setStateWithMinDuration = useCallback(
+ (newState: boolean) => {
+ if (newState) {
+ setState(true);
+ } else {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ timerRef.current = setTimeout(() => setState(false), minDuration);
+ }
+ },
+ [minDuration] // ✅ No dependency on timerRef, it won't change
+ );
useEffect(() => {
- return () => {
- if (timer) clearTimeout(timer);
+ return () : void => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
};
- }, [timer]);
+ }, []);
return [state, setStateWithMinDuration];
};
-export default function EmailsScreen() {
+/**
+ * Emails screen.
+ */
+export default function EmailsScreen() : React.ReactNode {
const dbContext = useDb();
const webApi = useWebApi();
const colors = useColors();
@@ -52,37 +67,9 @@ export default function EmailsScreen() {
const [isRefreshing, setIsRefreshing] = useState(false);
const [isTabFocused, setIsTabFocused] = useState(false);
- useEffect(() => {
- const unsubscribeFocus = navigation.addListener('focus', () => {
- setIsTabFocused(true);
- });
-
- const unsubscribeBlur = navigation.addListener('blur', () => {
- setIsTabFocused(false);
- });
-
- const sub = emitter.addListener('tabPress', (routeName: string) => {
- if (routeName === 'emails' && isTabFocused) {
- console.log('Emails tab re-pressed while focused: reset screen');
- // Scroll to top
- scrollViewRef.current?.scrollTo({ y: 0, animated: true });
- }
- });
-
- // Add listener for email refresh which other components can trigger,
- // e.g. the email delete event in email details screen.
- const refreshSub = emitter.addListener('refreshEmails', () => {
- loadEmails();
- });
-
- return () => {
- sub.remove();
- unsubscribeFocus();
- unsubscribeBlur();
- refreshSub.remove();
- };
- }, [isTabFocused]);
-
+ /**
+ * Load emails.
+ */
const loadEmails = useCallback(async () : Promise => {
try {
setError(null);
@@ -110,26 +97,96 @@ export default function EmailsScreen() {
setEmails(decryptedEmails);
setIsLoading(false);
- } catch (error) {
- console.error(error);
+ } catch {
throw new Error('Failed to load emails');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
- }, [dbContext?.sqliteClient, webApi]);
+ }, [dbContext?.sqliteClient, webApi, setIsLoading]);
+ useEffect(() => {
+ const unsubscribeFocus = navigation.addListener('focus', () => {
+ setIsTabFocused(true);
+ });
+
+ const unsubscribeBlur = navigation.addListener('blur', () => {
+ setIsTabFocused(false);
+ });
+
+ const sub = emitter.addListener('tabPress', (routeName: string) => {
+ if (routeName === 'emails' && isTabFocused) {
+ // Scroll to top
+ scrollViewRef.current?.scrollTo({ y: 0, animated: true });
+ }
+ });
+
+ /*
+ * Add listener for email refresh which other components can trigger,
+ * e.g. the email delete event in email details screen.
+ */
+ const refreshSub = emitter.addListener('refreshEmails', () => {
+ loadEmails();
+ });
+
+ return () : void => {
+ sub.remove();
+ unsubscribeFocus();
+ unsubscribeBlur();
+ refreshSub.remove();
+ };
+ }, [isTabFocused, loadEmails, navigation]);
+
+ /**
+ * Load emails on mount.
+ */
useEffect(() => {
loadEmails();
}, [loadEmails]);
- const onRefresh = useCallback(async () => {
+ /**
+ * Refresh the emails on pull to refresh.
+ */
+ const onRefresh = useCallback(async () : Promise => {
setIsRefreshing(true);
await loadEmails();
setIsRefreshing(false);
}, [loadEmails]);
- const renderContent = () => {
+ const styles = StyleSheet.create({
+ centerContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 20,
+ },
+ container: {
+ flex: 1,
+ },
+ content: {
+ flex: 1,
+ marginTop: 22,
+ padding: 16,
+ },
+ contentContainer: {
+ paddingBottom: 40,
+ paddingTop: 4,
+ },
+ emptyText: {
+ color: colors.textMuted,
+ opacity: 0.7,
+ textAlign: 'center',
+ },
+ errorText: {
+ color: colors.errorBackground,
+ textAlign: 'center',
+ },
+ });
+
+ /**
+ * Render the content.
+ */
+ const renderContent = () : React.ReactNode => {
if (isLoading) {
return (
@@ -150,7 +207,7 @@ export default function EmailsScreen() {
return (
- You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.
+ You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.
);
@@ -161,43 +218,6 @@ export default function EmailsScreen() {
));
};
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- content: {
- flex: 1,
- padding: 16,
- marginTop: 22,
- },
- headerImage: {
- color: colors.textMuted,
- bottom: -90,
- left: -35,
- position: 'absolute',
- },
- centerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: 20,
- },
- errorText: {
- color: colors.errorBackground,
- textAlign: 'center',
- },
- emptyText: {
- textAlign: 'center',
- opacity: 0.7,
- color: colors.textMuted,
- },
- refreshIndicator: {
- position: 'absolute',
- top: 20,
- alignSelf: 'center',
- },
- });
-
return (
(0);
useEffect(() => {
- const loadAutoLockTimeout = async () => {
+ /**
+ * Load the auto-lock timeout.
+ */
+ const loadAutoLockTimeout = async () : Promise => {
const timeout = await getAutoLockTimeout();
setAutoLockTimeoutState(timeout);
};
loadAutoLockTimeout();
- }, []);
+ }, [getAutoLockTimeout]);
const timeoutOptions = [
{ label: 'Never', value: 0 },
@@ -35,38 +42,38 @@ export default function AutoLockScreen() {
container: {
flex: 1,
},
- scrollView: {
+ header: {
+ borderBottomColor: colors.accentBorder,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ padding: 16,
+ },
+ headerText: {
+ color: colors.textMuted,
+ fontSize: 13,
+ },
+ option: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderBottomColor: colors.accentBorder,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ },
+ optionText: {
+ color: colors.text,
flex: 1,
+ fontSize: 16,
},
scrollContent: {
paddingBottom: 40,
},
- header: {
- padding: 16,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: colors.accentBorder,
- },
- headerText: {
- fontSize: 13,
- color: colors.textMuted,
- },
- option: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 16,
- paddingHorizontal: 16,
- backgroundColor: colors.accentBackground,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: colors.accentBorder,
- },
- optionText: {
+ scrollView: {
flex: 1,
- fontSize: 16,
- color: colors.text,
},
selectedIcon: {
- marginLeft: 8,
color: colors.primary,
+ marginLeft: 8,
},
});
@@ -78,7 +85,7 @@ export default function AutoLockScreen() {
>
- Choose how long the app can stay in the background before requiring re-authentication. You'll need to use Face ID or enter your password to unlock the vault again.
+ Choose how long the app can stay in the background before requiring re-authentication. You'll need to use Face ID or enter your password to unlock the vault again.
{timeoutOptions.map((option) => (
diff --git a/apps/mobile-app/app/(tabs)/settings/index.tsx b/apps/mobile-app/app/(tabs)/settings/index.tsx
index 9f76d7559..f210d7953 100644
--- a/apps/mobile-app/app/(tabs)/settings/index.tsx
+++ b/apps/mobile-app/app/(tabs)/settings/index.tsx
@@ -1,18 +1,23 @@
import { StyleSheet, View, ScrollView, TouchableOpacity, Image, Animated, Platform } from 'react-native';
+import { router } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { useRef, useState, useEffect } from 'react';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useWebApi } from '@/context/WebApiContext';
-import { router } from 'expo-router';
import { AppInfo } from '@/utils/AppInfo';
import { useColors } from '@/hooks/useColorScheme';
import { TitleContainer } from '@/components/TitleContainer';
import { useAuth } from '@/context/AuthContext';
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
-import { Ionicons } from '@expo/vector-icons';
import { CollapsibleHeader } from '@/components/CollapsibleHeader';
-import { useRef, useState, useEffect } from 'react';
+import avatarImage from '@/assets/images/avatar.webp';
-export default function SettingsScreen() {
+/**
+ * Settings screen.
+ */
+export default function SettingsScreen() : React.ReactNode {
const webApi = useWebApi();
const colors = useColors();
const { username, getAuthMethodDisplay, shouldShowIosAutofillReminder, getBiometricDisplayName } = useAuth();
@@ -21,163 +26,171 @@ export default function SettingsScreen() {
const scrollViewRef = useRef(null);
const [autoLockDisplay, setAutoLockDisplay] = useState('');
const [authMethodDisplay, setAuthMethodDisplay] = useState('');
- const [biometricDisplayName, setBiometricDisplayName] = useState('');
useEffect(() => {
- const loadAutoLockDisplay = async () => {
+ /**
+ * Load the auto-lock display.
+ */
+ const loadAutoLockDisplay = async () : Promise => {
const autoLockTimeout = await getAutoLockTimeout();
let display = 'Never';
- if (autoLockTimeout === 5) display = '5 seconds';
- else if (autoLockTimeout === 30) display = '30 seconds';
- else if (autoLockTimeout === 60) display = '1 minute';
- else if (autoLockTimeout === 900) display = '15 minutes';
- else if (autoLockTimeout === 1800) display = '30 minutes';
- else if (autoLockTimeout === 3600) display = '1 hour';
- else if (autoLockTimeout === 14400) display = '4 hours';
- else if (autoLockTimeout === 28800) display = '8 hours';
+ if (autoLockTimeout === 5) {
+ display = '5 seconds';
+ } else if (autoLockTimeout === 30) {
+ display = '30 seconds';
+ } else if (autoLockTimeout === 60) {
+ display = '1 minute';
+ } else if (autoLockTimeout === 900) {
+ display = '15 minutes';
+ } else if (autoLockTimeout === 1800) {
+ display = '30 minutes';
+ } else if (autoLockTimeout === 3600) {
+ display = '1 hour';
+ } else if (autoLockTimeout === 14400) {
+ display = '4 hours';
+ } else if (autoLockTimeout === 28800) {
+ display = '8 hours';
+ }
setAutoLockDisplay(display);
};
- const loadAuthMethodDisplay = async () => {
+ /**
+ * Load the auth method display.
+ */
+ const loadAuthMethodDisplay = async () : Promise => {
const authMethod = await getAuthMethodDisplay();
setAuthMethodDisplay(authMethod);
};
- const loadBiometricDisplayName = async () => {
- const displayName = await getBiometricDisplayName();
- setBiometricDisplayName(displayName);
- };
-
loadAutoLockDisplay();
loadAuthMethodDisplay();
- loadBiometricDisplayName();
}, [getAutoLockTimeout, getAuthMethodDisplay, getBiometricDisplayName]);
- const handleLogout = async () => {
+ /**
+ * Handle the logout.
+ */
+ const handleLogout = async () : Promise => {
await webApi.logout();
router.replace('/login');
};
- const handleVaultUnlockPress = () => {
+ /**
+ * Handle the vault unlock press.
+ */
+ const handleVaultUnlockPress = () : void => {
router.push('/(tabs)/settings/vault-unlock');
};
- const handleAutoLockPress = () => {
+ /**
+ * Handle the auto-lock press.
+ */
+ const handleAutoLockPress = () : void => {
router.push('/(tabs)/settings/auto-lock');
};
- const handleIosAutofillPress = () => {
+ /**
+ * Handle the iOS autofill press.
+ */
+ const handleIosAutofillPress = () : void => {
router.push('/(tabs)/settings/ios-autofill');
};
const styles = StyleSheet.create({
+ avatar: {
+ borderRadius: 20,
+ height: 40,
+ marginRight: 12,
+ width: 40,
+ },
container: {
flex: 1,
},
content: {
flex: 1,
- padding: 16,
marginTop: 22,
+ padding: 16,
+ },
+ scrollContent: {
+ paddingBottom: 40,
+ paddingTop: 4,
},
scrollView: {
flex: 1,
},
- scrollContent: {
- paddingBottom: 40,
- },
section: {
- marginTop: 20,
backgroundColor: colors.accentBackground,
borderRadius: 10,
+ marginTop: 20,
overflow: 'hidden',
},
- settingItem: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 6,
- paddingHorizontal: 16,
- backgroundColor: colors.accentBackground,
- },
- settingItemIcon: {
- width: 24,
- height: 24,
- marginRight: 12,
- justifyContent: 'center',
- alignItems: 'center',
- },
- settingItemContent: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 12,
- },
separator: {
- height: StyleSheet.hairlineWidth,
backgroundColor: colors.accentBorder,
+ height: StyleSheet.hairlineWidth,
marginLeft: 52,
},
- settingItemText: {
- flex: 1,
- fontSize: 16,
- color: colors.text,
- },
- settingItemValue: {
- fontSize: 16,
- color: colors.textMuted,
- marginRight: 8,
+ settingItem: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ paddingVertical: 6,
},
settingItemBadge: {
- backgroundColor: colors.primary,
- width: 16,
- height: 16,
- borderRadius: 8,
- justifyContent: 'center',
alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 8,
+ height: 16,
+ justifyContent: 'center',
marginRight: 8,
+ width: 16,
},
settingItemBadgeText: {
- color: '#FFFFFF',
+ color: colors.primarySurfaceText,
fontSize: 10,
fontWeight: '600',
- textAlign: 'center',
lineHeight: 16,
+ textAlign: 'center',
+ },
+ settingItemContent: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ paddingVertical: 12,
+ },
+ settingItemIcon: {
+ alignItems: 'center',
+ height: 24,
+ justifyContent: 'center',
+ marginRight: 12,
+ width: 24,
+ },
+ settingItemText: {
+ color: colors.text,
+ flex: 1,
+ fontSize: 16,
+ },
+ settingItemValue: {
+ color: colors.textMuted,
+ fontSize: 16,
+ marginRight: 8,
},
userInfoContainer: {
- flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background,
borderRadius: 10,
+ flexDirection: 'row',
marginBottom: 20,
},
- avatar: {
- width: 40,
- height: 40,
- borderRadius: 20,
- marginRight: 12,
- },
usernameText: {
+ color: colors.text,
fontSize: 16,
fontWeight: '600',
- color: colors.text,
- },
- logoutButton: {
- backgroundColor: '#FF3B30',
- padding: 16,
- borderRadius: 10,
- marginHorizontal: 16,
- marginTop: 20,
- },
- logoutButtonText: {
- color: '#FFFFFF',
- fontSize: 16,
- fontWeight: 'bold',
- textAlign: 'center',
},
versionContainer: {
- marginTop: 20,
alignItems: 'center',
+ marginTop: 20,
paddingBottom: 16,
},
versionText: {
@@ -202,14 +215,14 @@ export default function SettingsScreen() {
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
- contentContainerStyle={{ paddingBottom: 40, paddingTop: 4 }}
+ contentContainerStyle={styles.scrollContent}
scrollIndicatorInsets={{ bottom: 40 }}
style={styles.scrollView}
>
Logged in as: {username}
@@ -225,7 +238,7 @@ export default function SettingsScreen() {
-
+
iOS Autofill
{shouldShowIosAutofillReminder && (
@@ -244,7 +257,7 @@ export default function SettingsScreen() {
-
+
Vault Unlock Method
{authMethodDisplay}
@@ -258,7 +271,7 @@ export default function SettingsScreen() {
-
+
Auto-lock Timeout
{autoLockDisplay}
@@ -272,10 +285,10 @@ export default function SettingsScreen() {
onPress={handleLogout}
>
-
+
-
- Logout
+
+ Logout
diff --git a/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx b/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx
index 3bbfb3684..44926cbe3 100644
--- a/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx
+++ b/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx
@@ -1,101 +1,100 @@
import { StyleSheet, View, TouchableOpacity, ScrollView, Linking } from 'react-native';
+import { router } from 'expo-router';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useColors } from '@/hooks/useColorScheme';
import { useAuth } from '@/context/AuthContext';
-import { router } from 'expo-router';
-export default function IosAutofillScreen() {
+/**
+ * iOS autofill screen.
+ */
+export default function IosAutofillScreen() : React.ReactNode {
const colors = useColors();
const { markIosAutofillConfigured, shouldShowIosAutofillReminder } = useAuth();
- const handleConfigurePress = async () => {
+ /**
+ * Handle the configure press.
+ */
+ const handleConfigurePress = async () : Promise => {
await markIosAutofillConfigured();
await Linking.openURL('App-Prefs:root');
router.back();
};
- const handleAlreadyConfigured = async () => {
+ /**
+ * Handle the already configured press.
+ */
+ const handleAlreadyConfigured = async () : Promise => {
await markIosAutofillConfigured();
router.back();
};
const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- scrollView: {
- flex: 1,
- },
- scrollContent: {
- paddingBottom: 40,
- },
- header: {
- padding: 16,
- },
- headerText: {
- fontSize: 13,
- color: colors.textMuted,
- },
- instructionContainer: {
- padding: 16,
- },
- instructionTitle: {
- fontSize: 17,
- fontWeight: '600',
- color: colors.text,
- marginBottom: 8,
- },
- instructionStep: {
- fontSize: 15,
- color: colors.text,
- marginBottom: 8,
- lineHeight: 22,
- },
- warningText: {
- fontSize: 15,
- color: colors.textMuted,
- fontStyle: 'italic',
- marginTop: 8,
- },
buttonContainer: {
padding: 16,
paddingBottom: 32,
},
configureButton: {
- backgroundColor: colors.primary,
- paddingVertical: 16,
- borderRadius: 10,
alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 10,
+ paddingVertical: 16,
},
configureButtonText: {
- color: '#FFFFFF',
+ color: colors.primarySurfaceText,
fontSize: 16,
fontWeight: '600',
},
- noticeContainer: {
- backgroundColor: colors.accentBackground,
- padding: 16,
- margin: 16,
- borderRadius: 10,
+ container: {
+ flex: 1,
},
- noticeText: {
- fontSize: 15,
+ header: {
+ padding: 16,
+ },
+ headerText: {
+ color: colors.textMuted,
+ fontSize: 13,
+ },
+ instructionContainer: {
+ padding: 16,
+ },
+ instructionStep: {
color: colors.text,
- textAlign: 'center',
+ fontSize: 15,
+ lineHeight: 22,
+ marginBottom: 8,
+ },
+ instructionTitle: {
+ color: colors.text,
+ fontSize: 17,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ scrollContent: {
+ paddingBottom: 40,
+ },
+ scrollView: {
+ flex: 1,
},
secondaryButton: {
- backgroundColor: colors.accentBackground,
- paddingVertical: 16,
- borderRadius: 10,
alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderRadius: 10,
marginTop: 12,
+ paddingVertical: 16,
},
secondaryButtonText: {
color: colors.text,
fontSize: 16,
fontWeight: '600',
},
+ warningText: {
+ color: colors.textMuted,
+ fontSize: 15,
+ fontStyle: 'italic',
+ marginTop: 8,
+ },
});
return (
@@ -115,41 +114,41 @@ export default function IosAutofillScreen() {
1. Open iOS Settings via the button below
-
- 2. Go to "General"
-
-
- 3. Tap "AutoFill & Passwords"
-
-
- 4. Enable "AliasVault"
-
-
- 5. Disable other password providers (e.g. "iCloud Passwords") to avoid conflicts
-
-
- Note: You'll need to authenticate with Face ID/Touch ID or your device passcode when using autofill.
-
-
-
-
-
- Open Settings
-
-
- {shouldShowIosAutofillReminder && (
+
-
- I already configured it
+ style={styles.configureButton}
+ onPress={handleConfigurePress}
+ >
+
+ Open iOS Settings
- )}
+ {shouldShowIosAutofillReminder && (
+
+
+ I already configured it
+
+
+ )}
+
+
+ 2. Go to "General"
+
+
+ 3. Tap "AutoFill & Passwords"
+
+
+ 4. Enable "AliasVault"
+
+
+ 5. Disable other password providers (e.g. "iCloud Passwords") to avoid conflicts
+
+
+ Note: You'll need to authenticate with Face ID/Touch ID or your device passcode when using autofill.
+
diff --git a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx
index e1dca2de0..b01339e66 100644
--- a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx
+++ b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx
@@ -1,12 +1,16 @@
import { StyleSheet, View, ScrollView, Alert, Platform, Linking, Switch, TouchableOpacity } from 'react-native';
+import * as LocalAuthentication from 'expo-local-authentication';
+import { useState, useEffect, useCallback } from 'react';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
import { useColors } from '@/hooks/useColorScheme';
-import * as LocalAuthentication from 'expo-local-authentication';
-import { useState, useEffect, useMemo, useCallback } from 'react';
import { AuthMethod, useAuth } from '@/context/AuthContext';
-export default function VaultUnlockSettingsScreen() {
+/**
+ * Vault unlock settings screen.
+ */
+export default function VaultUnlockSettingsScreen() : React.ReactNode {
const colors = useColors();
const [initialized, setInitialized] = useState(false);
const { setAuthMethods, getEnabledAuthMethods, getBiometricDisplayName } = useAuth();
@@ -16,7 +20,10 @@ export default function VaultUnlockSettingsScreen() {
const [_, setEnabledAuthMethods] = useState([]);
useEffect(() => {
- const initializeAuth = async () => {
+ /**
+ * Initialize the auth methods.
+ */
+ const initializeAuth = async () : Promise => {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setHasFaceID(compatible && enrolled);
@@ -42,7 +49,10 @@ export default function VaultUnlockSettingsScreen() {
return;
}
- const updateAuthMethods = async () => {
+ /**
+ * Update the auth methods.
+ */
+ const updateAuthMethods = async () : Promise => {
const currentAuthMethods = await getEnabledAuthMethods();
const newAuthMethods = isFaceIDEnabled ? ['faceid', 'password'] : ['password'];
@@ -51,14 +61,13 @@ export default function VaultUnlockSettingsScreen() {
return;
}
- console.log('Updating auth methods to', newAuthMethods);
setAuthMethods(newAuthMethods as AuthMethod[]);
};
updateAuthMethods();
}, [isFaceIDEnabled, setAuthMethods, getEnabledAuthMethods, initialized]);
- const handleFaceIDToggle = useCallback(async (value: boolean) => {
+ const handleFaceIDToggle = useCallback(async (value: boolean) : Promise => {
if (value && !hasFaceID) {
Alert.alert(
'Face ID Not Available',
@@ -66,7 +75,10 @@ export default function VaultUnlockSettingsScreen() {
[
{
text: 'Open Settings',
- onPress: () => {
+ /**
+ * Handle the open settings press.
+ */
+ onPress: () : void => {
setIsFaceIDEnabled(true);
setAuthMethods(['faceid', 'password']);
if (Platform.OS === 'ios') {
@@ -77,7 +89,10 @@ export default function VaultUnlockSettingsScreen() {
{
text: 'Cancel',
style: 'cancel',
- onPress: () => {
+ /**
+ * Handle the cancel press.
+ */
+ onPress: () : void => {
setIsFaceIDEnabled(false);
setAuthMethods(['password']);
},
@@ -89,56 +104,56 @@ export default function VaultUnlockSettingsScreen() {
setIsFaceIDEnabled(value);
setAuthMethods(value ? ['faceid', 'password'] : ['password']);
- }, [hasFaceID]);
+ }, [hasFaceID, setAuthMethods]);
- const styles = useMemo(() => StyleSheet.create({
+ const styles = StyleSheet.create({
container: {
flex: 1,
},
- scrollView: {
- flex: 1,
- },
- scrollContent: {
- paddingBottom: 40,
- },
- header: {
- padding: 16,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: colors.accentBorder,
- },
- headerText: {
- fontSize: 13,
- color: colors.textMuted,
- },
- optionContainer: {
- backgroundColor: colors.background,
- },
- option: {
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: colors.accentBorder,
- },
- optionHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 4,
- },
- optionText: {
- fontSize: 16,
- color: colors.text,
- },
- helpText: {
- fontSize: 13,
- color: colors.textMuted,
- marginTop: 4,
- },
disabledText: {
color: colors.textMuted,
opacity: 0.5,
},
- }), [colors]);
+ header: {
+ borderBottomColor: colors.accentBorder,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ padding: 16,
+ },
+ headerText: {
+ color: colors.textMuted,
+ fontSize: 13,
+ },
+ helpText: {
+ color: colors.textMuted,
+ fontSize: 13,
+ marginTop: 4,
+ },
+ option: {
+ borderBottomColor: colors.accentBorder,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ optionContainer: {
+ backgroundColor: colors.background,
+ },
+ optionHeader: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 4,
+ },
+ optionText: {
+ color: colors.text,
+ fontSize: 16,
+ },
+ scrollContent: {
+ paddingBottom: 40,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ });
return (
diff --git a/apps/mobile-app/app/+not-found.tsx b/apps/mobile-app/app/+not-found.tsx
index 0aa54cef5..a6695d0b5 100644
--- a/apps/mobile-app/app/+not-found.tsx
+++ b/apps/mobile-app/app/+not-found.tsx
@@ -4,12 +4,15 @@ import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
-export default function NotFoundScreen() {
+/**
+ * Not found screen.
+ */
+export default function NotFoundScreen() : React.ReactNode {
return (
<>
- This screen doesn't exist.
+ This screen doesn't exist.
Go to home screen!
@@ -20,8 +23,8 @@ export default function NotFoundScreen() {
const styles = StyleSheet.create({
container: {
- flex: 1,
alignItems: 'center',
+ flex: 1,
justifyContent: 'center',
padding: 20,
},
diff --git a/apps/mobile-app/app/_layout.tsx b/apps/mobile-app/app/_layout.tsx
index 68b35d33a..5de3bb25e 100644
--- a/apps/mobile-app/app/_layout.tsx
+++ b/apps/mobile-app/app/_layout.tsx
@@ -4,9 +4,12 @@ import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
+
import 'react-native-reanimated';
-// Required for certain modules such as secure-remote-password which relies on crypto.getRandomValues
-// and this is not available in react-native without this polyfill
+/*
+ * Required for certain modules such as secure-remote-password which relies on crypto.getRandomValues
+ * and this is not available in react-native without this polyfill
+ */
import 'react-native-get-random-values';
import { useColors, useColorScheme } from '@/hooks/useColorScheme';
@@ -14,12 +17,15 @@ import { DbProvider } from '@/context/DbContext';
import { AuthProvider } from '@/context/AuthContext';
import { WebApiProvider } from '@/context/WebApiContext';
import { AliasVaultToast } from '@/components/Toast';
-import LoadingIndicator from '@/components/LoadingIndicator';
+import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
-function RootLayoutNav() {
+/**
+ * Root layout navigation.
+ */
+function RootLayoutNav() : React.ReactNode {
const colorScheme = useColorScheme();
const colors = useColors();
@@ -68,9 +74,12 @@ function RootLayoutNav() {
);
}
-export default function RootLayout() {
+/**
+ * Root layout.
+ */
+export default function RootLayout() : React.ReactNode {
const [loaded] = useFonts({
- SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
+ SpaceMono: SpaceMono,
});
useEffect(() => {
diff --git a/apps/mobile-app/app/index.tsx b/apps/mobile-app/app/index.tsx
index 744720663..bb46d1202 100644
--- a/apps/mobile-app/app/index.tsx
+++ b/apps/mobile-app/app/index.tsx
@@ -1,7 +1,11 @@
import { Redirect } from 'expo-router';
import { install } from 'react-native-quick-crypto';
-export default function AppIndex() {
+/**
+ * App index which is the entry point of the app and redirects to the sync screen, which will
+ * redirect to the login screen if the user is not logged in or to the main tabs screen if the user is logged in.
+ */
+export default function AppIndex() : React.ReactNode {
// Install the react-native-quick-crypto library which is used by the EncryptionUtility
install();
diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx
index 0776ab3a1..b57ea811f 100644
--- a/apps/mobile-app/app/login.tsx
+++ b/apps/mobile-app/app/login.tsx
@@ -1,10 +1,12 @@
-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';
+
+import React, { useState, useEffect } from 'react';
+import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, ActivityIndicator, Linking, Animated } from 'react-native';
import { router } from 'expo-router';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useFocusEffect } from '@react-navigation/native';
+import { MaterialIcons } from '@expo/vector-icons';
+
import { ThemedView } from '@/components/ThemedView';
import { useDb } from '@/context/DbContext';
import { useAuth } from '@/context/AuthContext';
@@ -16,15 +18,21 @@ import { useColors } from '@/hooks/useColorScheme';
import Logo from '@/assets/images/logo.svg';
import { AppInfo } from '@/utils/AppInfo';
import LoadingIndicator from '@/components/LoadingIndicator';
-import { MaterialIcons } from '@expo/vector-icons';
+import { LoginResponse } from '@/utils/types/webapi/Login';
+import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
-
-export default function LoginScreen() {
+/**
+ * Login screen.
+ */
+export default function LoginScreen() : React.ReactNode {
const colors = useColors();
const [fadeAnim] = useState(new Animated.Value(0));
const [apiUrl, setApiUrl] = useState(AppInfo.DEFAULT_API_URL);
- const loadApiUrl = async () => {
+ /**
+ * Load the API URL.
+ */
+ const loadApiUrl = async () : Promise => {
const storedUrl = await AsyncStorage.getItem('apiUrl');
if (storedUrl && storedUrl.length > 0) {
setApiUrl(storedUrl);
@@ -40,14 +48,17 @@ export default function LoginScreen() {
useNativeDriver: true,
}).start();
loadApiUrl();
- }, []);
+ }, [fadeAnim]);
// Update URL when returning from settings
useFocusEffect(() => {
loadApiUrl();
});
- const getDisplayUrl = () => {
+ /**
+ * Get the display URL.
+ */
+ const getDisplayUrl = () : string => {
const cleanUrl = apiUrl.replace('https://', '').replace('/api', '');
return cleanUrl === 'app.aliasvault.net' ? 'aliasvault.net' : cleanUrl;
};
@@ -61,7 +72,7 @@ export default function LoginScreen() {
const [error, setError] = useState(null);
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [twoFactorCode, setTwoFactorCode] = useState('');
- const [loginResponse, setLoginResponse] = useState(null);
+ const [loginResponse, setLoginResponse] = useState(null);
const [passwordHashString, setPasswordHashString] = useState(null);
const [passwordHashBase64, setPasswordHashBase64] = useState(null);
const [loginStatus, setLoginStatus] = useState(null);
@@ -82,9 +93,9 @@ export default function LoginScreen() {
const processVaultResponse = async (
token: string,
refreshToken: string,
- vaultResponseJson: any,
+ vaultResponseJson: VaultResponse,
passwordHashBase64: string
- ) => {
+ ) : Promise => {
await authContext.setAuthTokens(credentials.username, token, refreshToken);
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
await authContext.login();
@@ -99,7 +110,10 @@ export default function LoginScreen() {
setIsLoading(false);
};
- const handleSubmit = async () => {
+ /**
+ * Handle the submit.
+ */
+ const handleSubmit = async () : Promise => {
setIsLoading(true);
setError(null);
@@ -153,7 +167,7 @@ export default function LoginScreen() {
setLoginStatus('Syncing vault');
await new Promise(resolve => requestAnimationFrame(resolve));
- const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
+ const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
'Authorization': `Bearer ${validationResponse.token.token}`
} });
@@ -185,7 +199,10 @@ export default function LoginScreen() {
}
};
- const handleTwoFactorSubmit = async () => {
+ /**
+ * Handle the two factor submit.
+ */
+ const handleTwoFactorSubmit = async () : Promise => {
setIsLoading(true);
setLoginStatus('Verifying authentication code');
setError(null);
@@ -215,7 +232,7 @@ export default function LoginScreen() {
setLoginStatus('Syncing vault');
await new Promise(resolve => requestAnimationFrame(resolve));
- const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
+ const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: {
'Authorization': `Bearer ${validationResponse.token.token}`
} });
@@ -245,148 +262,142 @@ export default function LoginScreen() {
};
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: colors.background,
- },
- headerSection: {
- backgroundColor: colors.loginHeader,
- paddingTop: 24,
- paddingBottom: 24,
- paddingHorizontal: 16,
- borderBottomLeftRadius: 24,
- borderBottomRightRadius: 24,
- },
- logoContainer: {
- alignItems: 'center',
- marginBottom: 8,
- },
appName: {
+ color: colors.text,
fontSize: 32,
fontWeight: 'bold',
- color: colors.text,
textAlign: 'center',
},
- content: {
- flex: 1,
- padding: 16,
- backgroundColor: colors.background,
- },
- titleContainer: {
- flexDirection: 'row',
+ button: {
alignItems: 'center',
- gap: 8,
- marginBottom: 16,
- },
- formContainer: {
- gap: 16,
- },
- errorContainer: {
- backgroundColor: colors.errorBackground,
- borderColor: colors.errorBorder,
- borderWidth: 1,
+ borderRadius: 8,
padding: 12,
- borderRadius: 8,
- marginBottom: 16,
- },
- errorText: {
- color: colors.errorText,
- fontSize: 14,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- marginBottom: 4,
- color: colors.text,
- },
- inputContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- width: '100%',
- borderWidth: 1,
- borderColor: colors.accentBorder,
- borderRadius: 8,
- marginBottom: 16,
- backgroundColor: colors.accentBackground,
- },
- inputIcon: {
- padding: 10,
- },
- input: {
- flex: 1,
- height: 45,
- paddingHorizontal: 4,
- fontSize: 16,
- color: colors.text,
},
buttonContainer: {
gap: 8,
},
- button: {
- padding: 12,
- borderRadius: 8,
- alignItems: 'center',
- },
- primaryButton: {
- backgroundColor: colors.primary,
- },
- secondaryButton: {
- backgroundColor: colors.secondary,
- },
buttonText: {
color: colors.text,
fontSize: 16,
fontWeight: '600',
},
- rememberMeContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
checkbox: {
- width: 20,
- height: 20,
- borderWidth: 2,
- borderRadius: 4,
- justifyContent: 'center',
alignItems: 'center',
borderColor: colors.accentBorder,
- },
- checkboxInner: {
- width: 12,
- height: 12,
- borderRadius: 2,
+ borderRadius: 4,
+ borderWidth: 2,
+ height: 20,
+ justifyContent: 'center',
+ width: 20,
},
checkboxChecked: {
backgroundColor: colors.primary,
},
- rememberMeText: {
- fontSize: 14,
- color: colors.text,
- },
- noteText: {
- fontSize: 14,
- textAlign: 'center',
- marginTop: 16,
- color: colors.textMuted,
- },
- headerContainer: {
- marginBottom: 24,
- },
- headerTitle: {
- fontSize: 24,
- fontWeight: 'bold',
- color: colors.text,
- marginBottom: 4,
- },
- headerSubtitle: {
- fontSize: 14,
- color: colors.textMuted,
+ checkboxInner: {
+ borderRadius: 2,
+ height: 12,
+ width: 12,
},
clickableDomain: {
color: colors.primary,
textDecorationLine: 'underline',
},
+ container: {
+ backgroundColor: colors.background,
+ flex: 1,
+ },
+ content: {
+ backgroundColor: colors.background,
+ flex: 1,
+ padding: 16,
+ },
+ errorContainer: {
+ backgroundColor: colors.errorBackground,
+ borderColor: colors.errorBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ marginBottom: 16,
+ padding: 12,
+ },
+ errorText: {
+ color: colors.errorText,
+ fontSize: 14,
+ },
+ formContainer: {
+ gap: 16,
+ },
+ headerContainer: {
+ marginBottom: 24,
+ },
+ headerSection: {
+ backgroundColor: colors.loginHeader,
+ borderBottomLeftRadius: 24,
+ borderBottomRightRadius: 24,
+ paddingBottom: 24,
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ },
+ headerSubtitle: {
+ color: colors.textMuted,
+ fontSize: 14,
+ },
+ headerTitle: {
+ color: colors.text,
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ input: {
+ color: colors.text,
+ flex: 1,
+ fontSize: 16,
+ height: 45,
+ paddingHorizontal: 4,
+ },
+ inputContainer: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ flexDirection: 'row',
+ marginBottom: 16,
+ width: '100%',
+ },
+ inputIcon: {
+ padding: 10,
+ },
+ label: {
+ color: colors.text,
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ logoContainer: {
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ noteText: {
+ color: colors.textMuted,
+ fontSize: 14,
+ marginTop: 16,
+ textAlign: 'center',
+ },
+ primaryButton: {
+ backgroundColor: colors.primary,
+ },
+ rememberMeContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
+ },
+ rememberMeText: {
+ color: colors.text,
+ fontSize: 14,
+ },
+ secondaryButton: {
+ backgroundColor: colors.secondary,
+ },
});
return (
@@ -401,7 +412,7 @@ export default function LoginScreen() {
{isLoading ? (
-
+
) : (
<>
@@ -425,7 +436,7 @@ export default function LoginScreen() {
{twoFactorRequired ? (
- Authentication Code
+ Authentication Code
Cancel
-
- 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.
+
+ 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.
) : (
- Username or email
+ Username or email
- Password
+ Password
setRememberMe(!rememberMe)}
>
- Remember me
+ Remember me
Login
)}
-
+
No account yet?{' '}
(DEFAULT_OPTIONS[0].value);
const [customUrl, setCustomUrl] = useState('');
@@ -26,7 +29,10 @@ export default function SettingsScreen() {
loadStoredSettings();
}, []);
- const loadStoredSettings = async () => {
+ /**
+ * Load the stored settings.
+ */
+ const loadStoredSettings = async () : Promise => {
try {
const apiUrl = await AsyncStorage.getItem('apiUrl');
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
@@ -44,7 +50,10 @@ export default function SettingsScreen() {
}
};
- const handleOptionChange = async (value: string) => {
+ /**
+ * Handle the option change.
+ */
+ const handleOptionChange = async (value: string) : Promise => {
setSelectedOption(value);
if (value !== 'custom') {
await AsyncStorage.setItem('apiUrl', value);
@@ -52,7 +61,10 @@ export default function SettingsScreen() {
}
};
- const handleCustomUrlChange = async (value: string) => {
+ /**
+ * Handle the custom URL change.
+ */
+ const handleCustomUrlChange = async (value: string) : Promise => {
setCustomUrl(value);
await AsyncStorage.setItem('apiUrl', value);
};
@@ -66,41 +78,30 @@ export default function SettingsScreen() {
flex: 1,
padding: 16,
},
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginBottom: 24,
- },
- title: {
- fontSize: 24,
- fontWeight: 'bold',
- color: colors.text,
- },
formContainer: {
gap: 16,
},
+ input: {
+ backgroundColor: colors.accentBackground,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ color: colors.text,
+ fontSize: 16,
+ padding: 12,
+ },
label: {
+ color: colors.text,
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
- color: colors.text,
- },
- input: {
- borderWidth: 1,
- borderRadius: 8,
- padding: 12,
- fontSize: 16,
- borderColor: colors.accentBorder,
- color: colors.text,
- backgroundColor: colors.accentBackground,
},
optionButton: {
- padding: 12,
+ borderColor: colors.accentBorder,
borderRadius: 8,
borderWidth: 1,
- borderColor: colors.accentBorder,
marginBottom: 8,
+ padding: 12,
},
optionButtonSelected: {
backgroundColor: colors.primary,
@@ -113,10 +114,21 @@ export default function SettingsScreen() {
optionButtonTextSelected: {
color: colors.text,
},
+ title: {
+ color: colors.text,
+ fontSize: 24,
+ fontWeight: 'bold',
+ },
+ titleContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
+ marginBottom: 24,
+ },
versionText: {
- textAlign: 'center',
color: colors.textMuted,
marginTop: 24,
+ textAlign: 'center',
},
});
diff --git a/apps/mobile-app/app/sync.tsx b/apps/mobile-app/app/sync.tsx
index 162f4c0ba..d199c0f0b 100644
--- a/apps/mobile-app/app/sync.tsx
+++ b/apps/mobile-app/app/sync.tsx
@@ -1,13 +1,20 @@
import { useEffect, useRef, useState } from 'react';
import { router } from 'expo-router';
+import { StyleSheet } from 'react-native';
+
+import NativeVaultManager from '../specs/NativeVaultManager';
+
import { useAuth } from '@/context/AuthContext';
import { useVaultSync } from '@/hooks/useVaultSync';
import { ThemedView } from '@/components/ThemedView';
import LoadingIndicator from '@/components/LoadingIndicator';
import { useDb } from '@/context/DbContext';
-import NativeVaultManager from '../specs/NativeVaultManager';
-export default function SyncScreen() {
+/**
+ * Sync screen which will redirect to the login screen if the user is not logged in
+ * or to the credentials screen if the user is logged in.
+ */
+export default function SyncScreen() : React.ReactNode {
const authContext = useAuth();
const dbContext = useDb();
const { syncVault } = useVaultSync();
@@ -16,26 +23,29 @@ export default function SyncScreen() {
useEffect(() => {
if (hasInitialized.current) {
- return;
- }
+ return;
+ }
- hasInitialized.current = true;
+ hasInitialized.current = true;
-
- async function initialize() {
+ /**
+ * Initialize the app.
+ */
+ const initialize = async () : Promise => {
const { isLoggedIn, enabledAuthMethods } = await authContext.initializeAuth();
// If user is not logged in, navigate to login immediately
if (!isLoggedIn) {
- console.log('User not logged in, navigating to login');
router.replace('/login');
return;
}
// Perform initial vault sync
- console.log('Initial vault sync');
await syncVault({
initialSync: true,
+ /**
+ * Handle the status update.
+ */
onStatus: (message) => {
setStatus(message);
}
@@ -47,7 +57,6 @@ export default function SyncScreen() {
if (hasStoredVault) {
const isFaceIDEnabled = enabledAuthMethods.includes('faceid');
if (!isFaceIDEnabled) {
- console.log('FaceID is not enabled, navigating to unlock screen');
router.replace('/unlock');
return;
}
@@ -59,45 +68,52 @@ export default function SyncScreen() {
await new Promise(resolve => setTimeout(resolve, 1000));
setStatus('Decrypting vault');
await new Promise(resolve => setTimeout(resolve, 1000));
- console.log('FaceID unlock successful, navigating to credentials');
// Navigate to credentials
router.replace('/(tabs)/credentials');
return;
- }
- else {
- console.log('FaceID unlock failed, navigating to unlock screen');
+ } else {
router.replace('/unlock');
}
- }
- else {
- // Vault is not initialized which means the database does not exist or decryption key is missing
- // from device's keychain. Navigate to the unlock screen.
- console.log('Vault is not initialized (db file does not exist), navigating to unlock screen');
+ } else {
+ /*
+ * Vault is not initialized which means the database does not exist or decryption key is missing
+ * from device's keychain. Navigate to the unlock screen.
+ */
router.replace('/unlock');
return;
}
- } catch (error) {
- console.log('FaceID unlock failed:', error);
- // If FaceID fails (too many attempts, manual cancel, etc.)
- // navigate to unlock screen
+ } catch {
+ /*
+ * If FaceID fails (too many attempts, manual cancel, etc.)
+ * navigate to unlock screen
+ */
router.replace('/unlock');
return;
}
- // If we get here, something went wrong with the FaceID unlock
- // Navigate to unlock screen as a fallback
- console.log('FaceID unlock failed, navigating to unlock screen');
+ /*
+ * If we get here, something went wrong with the FaceID unlock
+ * Navigate to unlock screen as a fallback
+ */
router.replace('/unlock');
}
initialize();
- }, [syncVault]);
+ }, [syncVault, authContext, dbContext]);
return (
-
+
{status ? : null}
);
-}
\ No newline at end of file
+}
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+});
\ No newline at end of file
diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx
index 4eaf4c8bf..0aaf7a8c8 100644
--- a/apps/mobile-app/app/unlock.tsx
+++ b/apps/mobile-app/app/unlock.tsx
@@ -1,6 +1,8 @@
import { useState, useEffect } from 'react';
import { StyleSheet, View, TextInput, TouchableOpacity, Alert, Image, KeyboardAvoidingView, Platform } from 'react-native';
import { router } from 'expo-router';
+import { MaterialIcons } from '@expo/vector-icons';
+
import { useAuth } from '@/context/AuthContext';
import { useDb } from '@/context/DbContext';
import { ThemedView } from '@/components/ThemedView';
@@ -11,9 +13,12 @@ import Logo from '@/assets/images/logo.svg';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { SrpUtility } from '@/utils/SrpUtility';
import { useWebApi } from '@/context/WebApiContext';
-import { MaterialIcons } from '@expo/vector-icons';
+import avatarImage from '@/assets/images/avatar.webp';
-export default function UnlockScreen() {
+/**
+ * Unlock screen.
+ */
+export default function UnlockScreen() : React.ReactNode {
const { isLoggedIn, username, isFaceIDEnabled } = useAuth();
const { testDatabaseConnection } = useDb();
const [password, setPassword] = useState('');
@@ -24,14 +29,20 @@ export default function UnlockScreen() {
const srpUtil = new SrpUtility(webApi);
useEffect(() => {
- const checkFaceIDStatus = async () => {
+ /**
+ * Check the face ID status.
+ */
+ const checkFaceIDStatus = async () : Promise => {
const enabled = await isFaceIDEnabled();
setIsFaceIDAvailable(enabled);
};
checkFaceIDStatus();
}, [isFaceIDEnabled]);
- const handleUnlock = async () => {
+ /**
+ * Handle the unlock.
+ */
+ const handleUnlock = async () : Promise => {
if (!password) {
Alert.alert('Error', 'Please enter your password');
return;
@@ -48,8 +59,6 @@ export default function UnlockScreen() {
// Initialize the database with the provided password
const loginResponse = await srpUtil.initiateLogin(username);
- console.log('loginResponse', loginResponse);
-
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
password,
loginResponse.salt,
@@ -63,138 +72,145 @@ export default function UnlockScreen() {
if (await testDatabaseConnection(passwordHashBase64)) {
// Navigate to credentials
router.replace('/(tabs)/credentials');
- }
- else {
+ } else {
Alert.alert('Error', 'Incorrect password. Please try again.');
}
- } catch (error) {
+ } catch {
Alert.alert('Error', 'Incorrect password. Please try again.');
} finally {
setIsLoading(false);
}
};
- const handleLogout = async () => {
- // Clear any stored tokens or session data
- // This will be handled by the auth context
+ /**
+ * Handle the logout.
+ */
+ const handleLogout = async () : Promise => {
+ /*
+ * Clear any stored tokens or session data
+ * This will be handled by the auth context
+ */
await webApi.logout();
router.replace('/login');
};
- const handleFaceIDRetry = async () => {
+ /**
+ * Handle the face ID retry.
+ */
+ const handleFaceIDRetry = async () : Promise => {
router.replace('/');
};
const styles = StyleSheet.create({
+ avatar: {
+ borderRadius: 20,
+ height: 40,
+ marginRight: 12,
+ width: 40,
+ },
+ avatarContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginBottom: 16,
+ },
+ button: {
+ alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: 8,
+ height: 50,
+ justifyContent: 'center',
+ marginBottom: 16,
+ width: '100%',
+ },
+ buttonText: {
+ color: colors.primarySurfaceText,
+ fontSize: 16,
+ fontWeight: '600',
+ },
container: {
flex: 1,
},
- keyboardAvoidingView: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: 20,
- },
content: {
width: '100%',
},
- logoContainer: {
- alignItems: 'center',
- marginBottom: 16,
- },
- logo: {
- width: 200,
- height: 80,
- },
- title: {
- fontSize: 28,
- fontWeight: 'bold',
- marginBottom: 16,
- textAlign: 'center',
- color: colors.text,
- paddingTop: 4,
- },
- avatarContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 16,
- },
- avatar: {
- width: 40,
- height: 40,
- borderRadius: 20,
- marginRight: 12,
- },
- username: {
- fontSize: 18,
- textAlign: 'center',
- opacity: 0.8,
- color: colors.text,
- },
- subtitle: {
- fontSize: 16,
- marginBottom: 24,
- textAlign: 'center',
- opacity: 0.7,
- color: colors.text,
- },
- inputContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- width: '100%',
- borderWidth: 1,
- borderColor: colors.accentBorder,
- borderRadius: 8,
- marginBottom: 16,
- backgroundColor: colors.accentBackground,
- },
- inputIcon: {
- padding: 12,
- },
- input: {
- flex: 1,
- height: 50,
- paddingHorizontal: 16,
- fontSize: 16,
- color: colors.text,
- },
- button: {
- width: '100%',
- height: 50,
- backgroundColor: colors.primary,
- borderRadius: 8,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: 16,
- },
- buttonText: {
- color: 'white',
- fontSize: 16,
- fontWeight: '600',
- },
faceIdButton: {
- width: '100%',
+ alignItems: 'center',
height: 50,
justifyContent: 'center',
- alignItems: 'center',
marginBottom: 16,
+ width: '100%',
},
faceIdButtonText: {
color: colors.primary,
fontSize: 16,
fontWeight: '600',
},
- logoutButton: {
+ input: {
+ color: colors.text,
+ flex: 1,
+ fontSize: 16,
+ height: 50,
+ paddingHorizontal: 16,
+ },
+ inputContainer: {
+ alignItems: 'center',
+ backgroundColor: colors.accentBackground,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ flexDirection: 'row',
+ marginBottom: 16,
width: '100%',
+ },
+ inputIcon: {
+ padding: 12,
+ },
+ keyboardAvoidingView: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 20,
+ },
+ logo: {
+ height: 80,
+ width: 200,
+ },
+ logoContainer: {
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ logoutButton: {
+ alignItems: 'center',
height: 50,
justifyContent: 'center',
- alignItems: 'center',
+ width: '100%',
},
logoutButtonText: {
color: colors.primary,
fontSize: 16,
},
+ subtitle: {
+ color: colors.text,
+ fontSize: 16,
+ marginBottom: 24,
+ opacity: 0.7,
+ textAlign: 'center',
+ },
+ title: {
+ color: colors.text,
+ fontSize: 28,
+ fontWeight: 'bold',
+ marginBottom: 16,
+ paddingTop: 4,
+ textAlign: 'center',
+ },
+ username: {
+ color: colors.text,
+ fontSize: 18,
+ opacity: 0.8,
+ textAlign: 'center',
+ },
});
return (
@@ -213,7 +229,7 @@ export default function UnlockScreen() {
Unlock Vault
{username}
diff --git a/apps/mobile-app/assets.d.ts b/apps/mobile-app/assets.d.ts
new file mode 100644
index 000000000..c9d51b1fe
--- /dev/null
+++ b/apps/mobile-app/assets.d.ts
@@ -0,0 +1,30 @@
+// assets.d.ts
+declare module '*.png' {
+ const content: number;
+ export default content;
+ }
+
+ declare module '*.jpg' {
+ const content: number;
+ export default content;
+ }
+
+ declare module '*.jpeg' {
+ const content: number;
+ export default content;
+ }
+
+ declare module '*.gif' {
+ const content: number;
+ export default content;
+ }
+
+ declare module '*.webp' {
+ const content: number;
+ export default content;
+ }
+
+ declare module '*.ttf' {
+ const content: number;
+ export default content;
+ }
\ No newline at end of file
diff --git a/apps/mobile-app/components/credentialDetails/AliasDetails.tsx b/apps/mobile-app/components/credentialDetails/AliasDetails.tsx
index c04c18289..9908a9123 100644
--- a/apps/mobile-app/components/credentialDetails/AliasDetails.tsx
+++ b/apps/mobile-app/components/credentialDetails/AliasDetails.tsx
@@ -4,12 +4,15 @@ import { Credential } from '@/utils/types/Credential';
import FormInputCopyToClipboard from '@/components/FormInputCopyToClipboard';
import { IdentityHelperUtils } from '@/utils/shared/identity-generator';
-interface AliasDetailsProps {
+type AliasDetailsProps = {
credential: Credential;
-}
+};
-export const AliasDetails: React.FC = ({ credential }) => {
- const hasName = Boolean(credential.Alias?.FirstName?.trim() || credential.Alias?.LastName?.trim());
+/**
+ * Alias details component.
+ */
+export const AliasDetails: React.FC = ({ credential }) : React.ReactNode => {
+ const hasName = Boolean(credential.Alias?.FirstName?.trim() ?? credential.Alias?.LastName?.trim());
const fullName = [credential.Alias?.FirstName, credential.Alias?.LastName].filter(Boolean).join(' ');
if (!hasName && !credential.Alias?.NickName && !IdentityHelperUtils.isValidBirthDate(credential.Alias?.BirthDate)) {
diff --git a/apps/mobile-app/components/credentialDetails/EmailPreview.tsx b/apps/mobile-app/components/credentialDetails/EmailPreview.tsx
index e989db85b..50e528455 100644
--- a/apps/mobile-app/components/credentialDetails/EmailPreview.tsx
+++ b/apps/mobile-app/components/credentialDetails/EmailPreview.tsx
@@ -1,22 +1,26 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { View, StyleSheet, TouchableOpacity, Linking } from 'react-native';
+import { router } from 'expo-router';
+
import { useWebApi } from '@/context/WebApiContext';
import { useDb } from '@/context/DbContext';
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
import { MailboxBulkRequest, MailboxBulkResponse } from '@/utils/types/webapi/MailboxBulk';
import EncryptionUtility from '@/utils/EncryptionUtility';
-import { ThemedText } from '../ThemedText';
import { useColors } from '@/hooks/useColorScheme';
-import { router } from 'expo-router';
-import { ThemedView } from '../ThemedView';
import { AppInfo } from '@/utils/AppInfo';
import { PulseDot } from '@/components/PulseDot';
+import { ThemedText } from '@/components/ThemedText';
+import { ThemedView } from '@/components/ThemedView';
type EmailPreviewProps = {
email: string | undefined;
};
-export const EmailPreview: React.FC = ({ email }) => {
+/**
+ * Email preview component.
+ */
+export const EmailPreview: React.FC = ({ email }) : React.ReactNode => {
const [emails, setEmails] = useState([]);
const [loading, setLoading] = useState(true);
const [lastEmailId, setLastEmailId] = useState(0);
@@ -25,12 +29,10 @@ export const EmailPreview: React.FC = ({ email }) => {
const dbContext = useDb();
const colors = useColors();
- // Sanity check: if no email is provided, don't render anything.
- if (!email) {
- return null;
- }
-
- const isPublicDomain = async (emailAddress: string): Promise => {
+ /**
+ * Check if the email is a public domain.
+ */
+ const isPublicDomain = useCallback(async (emailAddress: string): Promise => {
// Get public domains from stored metadata
const metadata = await dbContext?.sqliteClient?.getVaultMetadata();
if (!metadata) {
@@ -38,11 +40,18 @@ export const EmailPreview: React.FC = ({ email }) => {
}
return metadata.publicEmailDomains.includes(emailAddress.split('@')[1]);
- };
+ }, [dbContext]);
useEffect(() => {
- const loadEmails = async () => {
+ /**
+ * Load the emails.
+ */
+ const loadEmails = async () : Promise => {
try {
+ if (!email) {
+ return;
+ }
+
const isPublic = await isPublicDomain(email);
setIsSpamOk(isPublic);
@@ -70,7 +79,9 @@ export const EmailPreview: React.FC = ({ email }) => {
setEmails(latestMails);
} else {
// For private domains, use existing encrypted email logic
- if (!dbContext?.sqliteClient) return;
+ if (!dbContext?.sqliteClient) {
+ return;
+ }
// Get all encryption keys
const encryptionKeys = await dbContext.sqliteClient.getAllEncryptionKeys();
@@ -113,48 +124,53 @@ export const EmailPreview: React.FC = ({ email }) => {
loadEmails();
// Set up auto-refresh interval
const interval = setInterval(loadEmails, 2000);
- return () => clearInterval(interval);
- }, [email, loading, webApi, dbContext]);
+ return () : void => clearInterval(interval);
+ }, [email, loading, webApi, dbContext, isPublicDomain]);
const styles = StyleSheet.create({
- section: {
- padding: 16,
- paddingBottom: 0,
+ date: {
+ color: colors.textMuted,
+ fontSize: 12,
+ opacity: 0.7,
+ },
+ emailItem: {
+ backgroundColor: colors.accentBackground,
+ borderColor: colors.accentBorder,
+ borderRadius: 8,
+ borderWidth: 1,
+ marginBottom: 12,
+ padding: 12,
},
placeholderText: {
color: colors.textMuted,
marginBottom: 8,
},
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- title: {
- fontSize: 20,
- fontWeight: 'bold',
- color: colors.text,
- },
- emailItem: {
- padding: 12,
- borderRadius: 8,
- marginBottom: 12,
- borderWidth: 1,
- borderColor: colors.accentBorder,
- backgroundColor: colors.accentBackground,
+ section: {
+ padding: 16,
+ paddingBottom: 0,
},
subject: {
- fontSize: 16,
color: colors.text,
+ fontSize: 16,
fontWeight: 'bold',
},
- date: {
- fontSize: 12,
- opacity: 0.7,
- color: colors.textMuted,
+ title: {
+ color: colors.text,
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+ titleContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
},
});
+ // Sanity check: if no email is provided, don't render anything.
+ if (!email) {
+ return null;
+ }
+
if (loading) {
return (
diff --git a/apps/mobile-app/components/credentialDetails/LoginCredentials.tsx b/apps/mobile-app/components/credentialDetails/LoginCredentials.tsx
index f9d1038ef..a8eb00170 100644
--- a/apps/mobile-app/components/credentialDetails/LoginCredentials.tsx
+++ b/apps/mobile-app/components/credentialDetails/LoginCredentials.tsx
@@ -1,19 +1,21 @@
-import { View } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { Credential } from '@/utils/types/Credential';
import FormInputCopyToClipboard from '@/components/FormInputCopyToClipboard';
-interface LoginCredentialsProps {
+type LoginCredentialsProps = {
credential: Credential;
-}
+};
-export const LoginCredentials: React.FC = ({ credential }) => {
+/**
+ * Login credentials component.
+ */
+export const LoginCredentials: React.FC = ({ credential }) : React.ReactNode => {
const email = credential.Alias?.Email?.trim();
const username = credential.Username?.trim();
const password = credential.Password?.trim();
- const hasLoginCredentials = email || username || password;
+ const hasLoginCredentials = email ?? username ?? password;
if (!hasLoginCredentials) {
return null;
diff --git a/apps/mobile-app/components/credentialDetails/NotesSection.tsx b/apps/mobile-app/components/credentialDetails/NotesSection.tsx
index 2a8d2c325..c311197ee 100644
--- a/apps/mobile-app/components/credentialDetails/NotesSection.tsx
+++ b/apps/mobile-app/components/credentialDetails/NotesSection.tsx
@@ -1,19 +1,20 @@
import { View, Text, useColorScheme, StyleSheet, Linking, Pressable } from 'react-native';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { Credential } from '@/utils/types/Credential';
-interface NotesSectionProps {
+type NotesSectionProps = {
credential: Credential;
-}
+};
/**
* Split text into parts, separating URLs from regular text to make them clickable.
*/
-const splitTextAndUrls = (text: string): Array<{ type: 'text' | 'url', content: string, url?: string }> => {
+const splitTextAndUrls = (text: string): { type: 'text' | 'url', content: string, url?: string }[] => {
const urlPattern = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g;
- const parts: Array<{ type: 'text' | 'url', content: string, url?: string }> = [];
+ const parts: { type: 'text' | 'url', content: string, url?: string }[] = [];
let lastIndex = 0;
let match;
@@ -55,7 +56,10 @@ const splitTextAndUrls = (text: string): Array<{ type: 'text' | 'url', content:
return parts;
};
-export const NotesSection: React.FC = ({ credential }) => {
+/**
+ * Notes section component.
+ */
+export const NotesSection: React.FC = ({ credential }) : React.ReactNode => {
const colorScheme = useColorScheme();
const isDarkMode = colorScheme === 'dark';
@@ -65,7 +69,10 @@ export const NotesSection: React.FC = ({ credential }) => {
const parts = splitTextAndUrls(credential.Notes);
- const handleLinkPress = (url: string) => {
+ /**
+ * Handle the link press.
+ */
+ const handleLinkPress = (url: string) : void => {
Linking.openURL(url);
};
@@ -103,21 +110,21 @@ export const NotesSection: React.FC = ({ credential }) => {
};
const styles = StyleSheet.create({
- section: {
- padding: 16,
- paddingBottom: 8,
- gap: 8,
- },
- notesContainer: {
- padding: 12,
- borderRadius: 8,
- borderWidth: 1,
- },
- notes: {
- fontSize: 14,
- },
link: {
fontSize: 14,
textDecorationLine: 'underline',
},
+ notes: {
+ fontSize: 14,
+ },
+ notesContainer: {
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 12,
+ },
+ section: {
+ gap: 8,
+ padding: 16,
+ paddingBottom: 8,
+ },
});
\ No newline at end of file
diff --git a/apps/mobile-app/components/credentialDetails/TotpSection.tsx b/apps/mobile-app/components/credentialDetails/TotpSection.tsx
index 8333f6257..2a86819bb 100644
--- a/apps/mobile-app/components/credentialDetails/TotpSection.tsx
+++ b/apps/mobile-app/components/credentialDetails/TotpSection.tsx
@@ -1,25 +1,32 @@
import React, { useState, useEffect } from 'react';
-import { View, StyleSheet, Pressable, useColorScheme, TouchableOpacity } from 'react-native';
+import { View, StyleSheet, useColorScheme, TouchableOpacity } from 'react-native';
+import * as OTPAuth from 'otpauth';
+import * as Clipboard from 'expo-clipboard';
+import Toast from 'react-native-toast-message';
+
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { Credential } from '@/utils/types/Credential';
import { TotpCode } from '@/utils/types/TotpCode';
-import * as OTPAuth from 'otpauth';
-import * as Clipboard from 'expo-clipboard';
import { useDb } from '@/context/DbContext';
-import Toast from 'react-native-toast-message';
type TotpSectionProps = {
credential: Credential;
};
-export const TotpSection: React.FC = ({ credential }) => {
+/**
+ * Totp section component.
+ */
+export const TotpSection: React.FC = ({ credential }) : React.ReactNode => {
const [totpCodes, setTotpCodes] = useState([]);
const [currentCodes, setCurrentCodes] = useState>({});
const colorScheme = useColorScheme();
const isDarkMode = colorScheme === 'dark';
const dbContext = useDb();
+ /**
+ * Get the remaining seconds.
+ */
const getRemainingSeconds = (step = 30): number => {
const totp = new OTPAuth.TOTP({
secret: 'dummy',
@@ -30,11 +37,17 @@ export const TotpSection: React.FC = ({ credential }) => {
return totp.period - (Math.floor(Date.now() / 1000) % totp.period);
};
+ /**
+ * Get the remaining percentage.
+ */
const getRemainingPercentage = (): number => {
const remaining = getRemainingSeconds();
return Math.floor(((30.0 - remaining) / 30.0) * 100);
};
+ /**
+ * Generate the totp code.
+ */
const generateTotpCode = (secretKey: string): string => {
try {
const totp = new OTPAuth.TOTP({
@@ -50,6 +63,9 @@ export const TotpSection: React.FC = ({ credential }) => {
}
};
+ /**
+ * Copy the totp code to the clipboard.
+ */
const copyToClipboard = async (code: string): Promise => {
try {
await Clipboard.setStringAsync(code);
@@ -65,7 +81,10 @@ export const TotpSection: React.FC = ({ credential }) => {
};
useEffect(() => {
- const loadTotpCodes = async () => {
+ /**
+ * Load the totp codes.
+ */
+ const loadTotpCodes = async () : Promise => {
if (!dbContext?.sqliteClient) {
return;
}
@@ -82,6 +101,9 @@ export const TotpSection: React.FC = ({ credential }) => {
}, [credential.Id, dbContext?.sqliteClient]);
useEffect(() => {
+ /**
+ * Update the totp codes.
+ */
const updateTotpCodes = (prevCodes: Record): Record => {
const newCodes: Record = {};
totpCodes.forEach(code => {
@@ -105,7 +127,7 @@ export const TotpSection: React.FC = ({ credential }) => {
setCurrentCodes(updateTotpCodes);
}, 1000);
- return () => clearInterval(intervalId);
+ return () : void => clearInterval(intervalId);
}, [totpCodes]);
if (totpCodes.length === 0) {
@@ -159,46 +181,46 @@ export const TotpSection: React.FC = ({ credential }) => {
};
const styles = StyleSheet.create({
- container: {
- padding: 16,
- marginTop: 16,
- },
- content: {
- marginTop: 8,
- borderRadius: 8,
- borderWidth: 1,
- padding: 12,
- },
- codeContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- label: {
- fontSize: 12,
- marginBottom: 4,
- },
code: {
fontSize: 24,
fontWeight: 'bold',
letterSpacing: 2,
},
- timerContainer: {
- alignItems: 'flex-end',
+ codeContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ container: {
+ marginTop: 16,
+ padding: 16,
+ },
+ content: {
+ borderRadius: 8,
+ borderWidth: 1,
+ marginTop: 8,
+ padding: 12,
+ },
+ label: {
+ fontSize: 12,
+ marginBottom: 4,
+ },
+ progressBar: {
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
+ borderRadius: 2,
+ height: 4,
+ overflow: 'hidden',
+ width: 40,
+ },
+ progressFill: {
+ backgroundColor: '#007AFF',
+ height: '100%',
},
timer: {
fontSize: 12,
marginBottom: 4,
},
- progressBar: {
- width: 40,
- height: 4,
- backgroundColor: 'rgba(0, 0, 0, 0.1)',
- borderRadius: 2,
- overflow: 'hidden',
- },
- progressFill: {
- height: '100%',
- backgroundColor: '#007AFF',
+ timerContainer: {
+ alignItems: 'flex-end',
},
});
\ No newline at end of file
diff --git a/apps/mobile-app/constants/Colors.ts b/apps/mobile-app/constants/Colors.ts
index 3152b0c1e..2d9ef176d 100644
--- a/apps/mobile-app/constants/Colors.ts
+++ b/apps/mobile-app/constants/Colors.ts
@@ -20,11 +20,13 @@ export const Colors = {
headerBackground: '#fff',
tabBarBackground: '#fff',
primary: '#f49541',
+ primarySurfaceText: '#ffffff',
secondary: '#6b7280',
tertiary: '#eabf69',
loginHeader: '#f6dfc4',
},
dark: {
+ white: '#ffffff',
text: '#ECEDEE',
textMuted: '#9BA1A6',
background: '#111827',
@@ -40,6 +42,7 @@ export const Colors = {
headerBackground: '#1f2937',
tabBarBackground: '#1f2937',
primary: '#f49541',
+ primarySurfaceText: '#ffffff',
secondary: '#6b7280',
tertiary: '#eabf69',
loginHeader: '#5c4331',