mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-19 07:28:47 -05:00
Add random prefix email generation for login type to mobile app (#1449)
This commit is contained in:
@@ -10,7 +10,7 @@ import { StyleSheet, View, Keyboard, Platform, ScrollView, KeyboardAvoidingView,
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { Folder } from '@/utils/db/repositories/FolderRepository';
|
||||
import { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, Identity, IdentityHelperUtils, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator';
|
||||
import { CreateIdentityGenerator, CreateUsernameEmailGenerator, UsernameEmailGenerator, Gender, Identity, IdentityHelperUtils, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator';
|
||||
import type { Attachment, Item, ItemField, TotpCode, ItemType, FieldType, PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
import { ItemTypes, getSystemFieldsForItemType, getOptionalFieldsForItemType, isFieldShownByDefault, getSystemField, fieldAppliesToType, FieldCategories, FieldTypes } from '@/utils/dist/core/models/vault';
|
||||
import type { FaviconExtractModel } from '@/utils/dist/core/models/webapi';
|
||||
@@ -387,6 +387,48 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
}
|
||||
}, [fieldValues, generateRandomIdentity, handleFieldChange]);
|
||||
|
||||
/**
|
||||
* Generate an identity-based email alias (for Alias type email field).
|
||||
* Uses the current alias field values (first name, last name, birthdate) to derive the email prefix,
|
||||
* so the email stays consistent with the filled-in persona fields.
|
||||
*/
|
||||
const handleGenerateAliasEmail = useCallback(async () => {
|
||||
const firstName = (fieldValues['alias.first_name'] as string) || '';
|
||||
const lastName = (fieldValues['alias.last_name'] as string) || '';
|
||||
const gender = (fieldValues['alias.gender'] as string) || Gender.Other;
|
||||
const birthdate = (fieldValues['alias.birthdate'] as string) || '';
|
||||
|
||||
const generator = new UsernameEmailGenerator();
|
||||
const prefix = generator.generateEmailPrefix({
|
||||
firstName,
|
||||
lastName,
|
||||
gender: gender as Gender,
|
||||
birthDate: birthdate ? new Date(birthdate) : new Date(),
|
||||
emailPrefix: '',
|
||||
nickName: ''
|
||||
});
|
||||
|
||||
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${prefix}@${defaultEmailDomain}` : prefix;
|
||||
|
||||
handleFieldChange('login.email', email);
|
||||
}, [fieldValues, dbContext.sqliteClient, handleFieldChange]);
|
||||
|
||||
/**
|
||||
* Generate a random-string email alias (for Login type email field).
|
||||
* Uses random characters instead of identity-based prefixes since Login type
|
||||
* has no persona fields to base the email on.
|
||||
*/
|
||||
const handleGenerateRandomEmail = useCallback(async () => {
|
||||
const generator = new UsernameEmailGenerator();
|
||||
const prefix = generator.generateRandomEmailPrefix();
|
||||
|
||||
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${prefix}@${defaultEmailDomain}` : prefix;
|
||||
|
||||
handleFieldChange('login.email', email);
|
||||
}, [dbContext.sqliteClient, handleFieldChange]);
|
||||
|
||||
/**
|
||||
* Prevent accidental dismissal when there are unsaved changes.
|
||||
* Shows custom dialog on Android, stores pending action for later execution.
|
||||
@@ -1111,6 +1153,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
onRemove={onRemove}
|
||||
testID={testID}
|
||||
defaultEmailMode={defaultEmailMode}
|
||||
onGenerateAlias={aliasFieldsShownByDefault ? handleGenerateAliasEmail : handleGenerateRandomEmail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1163,7 +1206,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [fieldValues, handleFieldChange, isPasswordVisible, isEditMode, aliasFieldsShownByDefault, generateRandomUsername, t, getFieldTestId, item?.ItemType, passwordSettings]);
|
||||
}, [fieldValues, handleFieldChange, isPasswordVisible, isEditMode, aliasFieldsShownByDefault, generateRandomUsername, handleGenerateAliasEmail, handleGenerateRandomEmail, t, getFieldTestId, item?.ItemType, passwordSettings]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { CreateIdentityGenerator, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator';
|
||||
|
||||
type EmailDomainFieldProps = {
|
||||
value: string;
|
||||
@@ -21,6 +20,8 @@ type EmailDomainFieldProps = {
|
||||
testID?: string;
|
||||
/** Optional: default to email mode (free text) instead of alias mode (domain chooser). Defaults to false. */
|
||||
defaultEmailMode?: boolean;
|
||||
/** Optional callback to generate an email alias. When provided, shows a regenerate button and is called when switching to alias mode. */
|
||||
onGenerateAlias?: () => void;
|
||||
}
|
||||
|
||||
// Hardcoded public email domains (same as in browser extension)
|
||||
@@ -49,12 +50,13 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
label,
|
||||
onRemove,
|
||||
testID,
|
||||
defaultEmailMode = false
|
||||
defaultEmailMode = false,
|
||||
onGenerateAlias
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const colors = useColors();
|
||||
const dbContext = useDb();
|
||||
|
||||
|
||||
// Initialize mode immediately based on value (before domains load)
|
||||
// This prevents flicker by setting the correct mode right away
|
||||
const getInitialMode = useCallback((val: string): boolean => {
|
||||
@@ -84,16 +86,16 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
return PUBLIC_EMAIL_DOMAINS[0] || '';
|
||||
});
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
|
||||
// Use refs to store domains - this prevents re-renders when they load
|
||||
const privateEmailDomainsRef = useRef<string[]>([]);
|
||||
const hiddenPrivateEmailDomainsRef = useRef<string[]>([]);
|
||||
const hasDomainsLoadedRef = useRef(false);
|
||||
|
||||
|
||||
// State for domains (only used for UI display in modal, not for mode detection)
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
|
||||
|
||||
|
||||
// Track value changes to avoid unnecessary re-initialization
|
||||
const lastValueRef = useRef<string>(value);
|
||||
const hasInitializedFromValue = useRef(false);
|
||||
@@ -110,16 +112,16 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
const privateDomains = metadata?.privateEmailDomains ?? [];
|
||||
const hiddenDomains = metadata?.hiddenPrivateEmailDomains ?? [];
|
||||
|
||||
|
||||
// Update refs immediately (no re-render triggered)
|
||||
privateEmailDomainsRef.current = privateDomains;
|
||||
hiddenPrivateEmailDomainsRef.current = hiddenDomains;
|
||||
hasDomainsLoadedRef.current = true;
|
||||
|
||||
|
||||
// Update state for modal display (triggers re-render, but only affects modal)
|
||||
setPrivateEmailDomains(privateDomains);
|
||||
setHiddenPrivateEmailDomains(hiddenDomains);
|
||||
|
||||
|
||||
// Check if we need to update mode now that domains are loaded
|
||||
// Only update if value has an @ and we're in custom mode
|
||||
if (value && value.includes('@')) {
|
||||
@@ -128,7 +130,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
|
||||
privateDomains.includes(domain) ||
|
||||
hiddenDomains.includes(domain);
|
||||
|
||||
|
||||
// Only update mode if domain is now recognized AND we're in custom mode
|
||||
// Use functional update to avoid stale closure and prevent unnecessary updates
|
||||
setIsCustomDomain(prev => {
|
||||
@@ -174,7 +176,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
|
||||
if (value.includes('@')) {
|
||||
const [local, domain] = value.split('@');
|
||||
|
||||
|
||||
// Only update if values actually changed (prevents unnecessary re-renders)
|
||||
if (local !== localPart) {
|
||||
setLocalPart(local);
|
||||
@@ -182,18 +184,18 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
if (domain !== selectedDomain) {
|
||||
setSelectedDomain(domain);
|
||||
}
|
||||
|
||||
|
||||
// Check mode using refs (no re-render) - works even before domains load
|
||||
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
|
||||
privateEmailDomainsRef.current.includes(domain) ||
|
||||
hiddenPrivateEmailDomainsRef.current.includes(domain);
|
||||
|
||||
|
||||
// Only update mode if it needs to change (prevents flicker)
|
||||
setIsCustomDomain(prev => {
|
||||
const newMode = !isKnownDomain;
|
||||
return newMode !== prev ? newMode : prev;
|
||||
});
|
||||
|
||||
|
||||
hasInitializedFromValue.current = true;
|
||||
} else {
|
||||
if (value !== localPart) {
|
||||
@@ -259,58 +261,42 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
setIsModalVisible(false);
|
||||
}, [localPart, onChange]);
|
||||
|
||||
/**
|
||||
* Generate a random email prefix using identity generator.
|
||||
*/
|
||||
const generateRandomEmailPrefix = useCallback(async (): Promise<string> => {
|
||||
try {
|
||||
const identityLanguage = await dbContext.sqliteClient!.getEffectiveIdentityLanguage();
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage);
|
||||
|
||||
const genderPreference = await dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
const ageRange = await dbContext.sqliteClient!.getDefaultIdentityAgeRange();
|
||||
const birthdateOptions = convertAgeRangeToBirthdateOptions(ageRange);
|
||||
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference, birthdateOptions);
|
||||
return identity.emailPrefix;
|
||||
} catch (error) {
|
||||
console.error('Error generating random email prefix:', error);
|
||||
// Fallback to a simple random string if generation fails
|
||||
return `user${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
}, [dbContext]);
|
||||
|
||||
// Toggle between custom domain and domain chooser
|
||||
const toggleCustomDomain = useCallback(async () => {
|
||||
const toggleCustomDomain = useCallback(() => {
|
||||
const newIsCustom = !isCustomDomain;
|
||||
setIsCustomDomain(newIsCustom);
|
||||
|
||||
if (newIsCustom) {
|
||||
// Switching to custom domain mode
|
||||
// If we have a domain-based value, extract just the local part
|
||||
if (value && value.includes('@')) {
|
||||
const [local] = value.split('@');
|
||||
onChange(local);
|
||||
setLocalPart(local);
|
||||
}
|
||||
// Switching to custom domain mode (free text / normal email).
|
||||
// Clear the value so the user starts fresh with a regular email address.
|
||||
onChange('');
|
||||
setLocalPart('');
|
||||
} else {
|
||||
// Switching to domain chooser mode - generate a random email prefix
|
||||
// Switching to domain chooser mode
|
||||
setIsCustomDomain(false);
|
||||
|
||||
if (onGenerateAlias) {
|
||||
// Delegate to the parent callback which sets the full email value (prefix@domain)
|
||||
onGenerateAlias();
|
||||
return;
|
||||
}
|
||||
|
||||
// No generate callback - just switch modes and preserve current local part
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: PUBLIC_EMAIL_DOMAINS[0];
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
// Generate a random email prefix instead of reusing the old one
|
||||
const randomPrefix = await generateRandomEmailPrefix();
|
||||
setLocalPart(randomPrefix);
|
||||
onChange(`${randomPrefix}@${defaultDomain}`);
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
setLocalPart(value);
|
||||
}
|
||||
}
|
||||
}, [isCustomDomain, value, showPrivateDomains, privateEmailDomains, onChange, generateRandomEmailPrefix]);
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange, onGenerateAlias]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
domainAt: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 16,
|
||||
@@ -329,6 +315,22 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 11,
|
||||
},
|
||||
domainButtonNoRoundRight: {
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
},
|
||||
generateButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderBottomRightRadius: 8,
|
||||
borderColor: error ? colors.errorBorder : colors.accentBorder,
|
||||
borderLeftWidth: 0,
|
||||
borderTopRightRadius: 8,
|
||||
borderWidth: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 11,
|
||||
},
|
||||
domainButtonText: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
@@ -482,7 +484,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View>
|
||||
<View style={styles.labelContainer}>
|
||||
<View style={styles.switcherContainer}>
|
||||
<TouchableOpacity
|
||||
@@ -535,7 +537,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
|
||||
{!isCustomDomain && (
|
||||
<TouchableOpacity
|
||||
style={styles.domainButton}
|
||||
style={[styles.domainButton, onGenerateAlias ? styles.domainButtonNoRoundRight : null]}
|
||||
onPress={() => setIsModalVisible(true)}
|
||||
>
|
||||
<Text style={styles.domainAt}>@</Text>
|
||||
@@ -544,6 +546,16 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{!isCustomDomain && onGenerateAlias && (
|
||||
<TouchableOpacity
|
||||
style={styles.generateButton}
|
||||
onPress={onGenerateAlias}
|
||||
accessibilityLabel={t('common.generate')}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"disabled": "Disabled",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"add": "Add",
|
||||
"generate": "Generate",
|
||||
"attachments": "Attachments",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
|
||||
Reference in New Issue
Block a user