Tweak email domain mode switching logic (#1449)
This commit is contained in:
+39
-60
@@ -73,6 +73,12 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/**
|
||||
* Tracks whether the user explicitly toggled mode via buttons.
|
||||
* While true, the value useEffect skips auto-detection of isCustomDomain.
|
||||
*/
|
||||
const modeToggledByUser = useRef(false);
|
||||
|
||||
// Get email domains from vault metadata
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -115,21 +121,19 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
setSelectedDomain(domain);
|
||||
|
||||
/*
|
||||
* Auto-detect mode based on domain recognition.
|
||||
* In controlled mode, notify parent via onEmailModeChange.
|
||||
* In uncontrolled mode, update internal state directly.
|
||||
* Auto-detect mode based on domain recognition, but only if the user
|
||||
* hasn't explicitly toggled mode via the Email/Alias buttons.
|
||||
*/
|
||||
// Check if it's a known domain (public, private, or hidden private)
|
||||
const isKnownDomain = publicEmailDomains.includes(domain) ||
|
||||
privateEmailDomains.includes(domain) ||
|
||||
hiddenPrivateEmailDomains.includes(domain);
|
||||
if (!modeToggledByUser.current) {
|
||||
const isKnownDomain = publicEmailDomains.includes(domain) ||
|
||||
privateEmailDomains.includes(domain) ||
|
||||
hiddenPrivateEmailDomains.includes(domain);
|
||||
|
||||
if (isControlled && onEmailModeChange) {
|
||||
// Controlled mode: notify parent that mode should be alias (domain chooser) if domain is known
|
||||
onEmailModeChange(!isKnownDomain);
|
||||
} else if (!isControlled) {
|
||||
// Uncontrolled mode: update internal state directly
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
if (isControlled && onEmailModeChange) {
|
||||
onEmailModeChange(!isKnownDomain);
|
||||
} else if (!isControlled) {
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setLocalPart(value);
|
||||
@@ -149,9 +153,13 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
/*
|
||||
* Re-check domain mode when domains finish loading.
|
||||
* This handles the case where value was set before domains were loaded.
|
||||
* Works in both controlled and uncontrolled modes.
|
||||
* Skip if the user has explicitly toggled mode via buttons.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (modeToggledByUser.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value || !value.includes('@')) {
|
||||
return;
|
||||
}
|
||||
@@ -161,19 +169,13 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the domain is now recognized after domains loaded
|
||||
const isKnownDomain = publicEmailDomains.includes(domain) ||
|
||||
privateEmailDomains.includes(domain) ||
|
||||
hiddenPrivateEmailDomains.includes(domain);
|
||||
|
||||
if (isControlled && onEmailModeChange) {
|
||||
// Controlled mode: notify parent that mode should be alias (domain chooser) if domain is known
|
||||
onEmailModeChange(!isKnownDomain);
|
||||
} else if (!isControlled) {
|
||||
/*
|
||||
* Uncontrolled mode: update internal state directly.
|
||||
* If domain is recognized and we're in custom mode, switch to domain chooser.
|
||||
*/
|
||||
if (isKnownDomain && isCustomDomain) {
|
||||
setIsCustomDomain(false);
|
||||
}
|
||||
@@ -219,6 +221,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
// Handle local part changes
|
||||
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newLocalPart = e.target.value;
|
||||
modeToggledByUser.current = false;
|
||||
|
||||
// If in custom domain mode, always pass through the full value
|
||||
if (isCustomDomain) {
|
||||
@@ -277,6 +280,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
|
||||
// Toggle between custom domain and domain chooser
|
||||
const toggleCustomDomain = useCallback(() => {
|
||||
modeToggledByUser.current = true;
|
||||
const newIsCustom = !isCustomDomain;
|
||||
setIsCustomDomain(newIsCustom);
|
||||
|
||||
@@ -288,31 +292,15 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
onChange('');
|
||||
setLocalPart('');
|
||||
} else {
|
||||
// Switching to domain chooser mode
|
||||
// Switching to domain chooser mode - clear the old email-mode value.
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: publicEmailDomains[0];
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
/*
|
||||
* Use the same simple pattern as mobile app:
|
||||
* 1. Check localPart first (most reliable, kept in sync by useEffect)
|
||||
* 2. Fallback to value if it doesn't have @ (value is just a prefix)
|
||||
*
|
||||
* Note: If value has @, the useEffect will have already extracted and set localPart,
|
||||
* so checking localPart first is the right approach.
|
||||
*/
|
||||
if (localPart && localPart.trim()) {
|
||||
// localPart is available - use it directly
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
// Fallback: value is just a prefix without @
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
// Also update localPart to keep in sync
|
||||
setLocalPart(value);
|
||||
}
|
||||
setLocalPart('');
|
||||
onChange('');
|
||||
}
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, publicEmailDomains, privateEmailDomains, onChange, setIsCustomDomain]);
|
||||
}, [isCustomDomain, showPrivateDomains, publicEmailDomains, privateEmailDomains, onChange, setIsCustomDomain]);
|
||||
|
||||
// Handle clicks outside the popup
|
||||
useEffect(() => {
|
||||
@@ -339,34 +327,25 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
* Otherwise, just switches to domain chooser mode and preserves the current local part.
|
||||
*/
|
||||
const handleGenerateAliasClick = useCallback(() => {
|
||||
// Always switch to domain chooser mode
|
||||
modeToggledByUser.current = true;
|
||||
setIsCustomDomain(false);
|
||||
|
||||
if (onGenerateAlias) {
|
||||
// Delegate to the parent callback which sets the full email value (prefix@domain)
|
||||
onGenerateAlias();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* No generate callback - just switching modes.
|
||||
* Ensure the value includes the domain when switching from email to alias mode.
|
||||
*/
|
||||
// Reset to the default domain so stale domains from email mode are cleared.
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: publicEmailDomains[0];
|
||||
|
||||
if (defaultDomain) {
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
setLocalPart(value);
|
||||
}
|
||||
}
|
||||
}, [onGenerateAlias, setIsCustomDomain, showPrivateDomains, privateEmailDomains, publicEmailDomains, value, localPart, onChange]);
|
||||
|
||||
// Clear the old email-mode value so it doesn't interfere with alias mode.
|
||||
setLocalPart('');
|
||||
onChange('');
|
||||
|
||||
if (onGenerateAlias) {
|
||||
onGenerateAlias();
|
||||
}
|
||||
}, [onGenerateAlias, setIsCustomDomain, showPrivateDomains, privateEmailDomains, publicEmailDomains, onChange]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -518,18 +518,25 @@ const ItemAddEdit: React.FC = () => {
|
||||
|
||||
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: ''
|
||||
});
|
||||
let prefix: string;
|
||||
if (!firstName.trim() && !lastName.trim()) {
|
||||
// No alias identity fields filled in, fall back to random prefix.
|
||||
prefix = generateRandomEmailPrefix();
|
||||
} else {
|
||||
const gender = (fieldValues['alias.gender'] as string) || Gender.Other;
|
||||
const birthdate = (fieldValues['alias.birthdate'] as string) || '';
|
||||
|
||||
const generator = new UsernameEmailGenerator();
|
||||
prefix = generator.generateEmailPrefix({
|
||||
firstName,
|
||||
lastName,
|
||||
gender: gender as Gender,
|
||||
birthDate: birthdate ? new Date(birthdate) : new Date(),
|
||||
emailPrefix: '',
|
||||
nickName: ''
|
||||
});
|
||||
}
|
||||
|
||||
const defaultEmailDomain = dbContext.sqliteClient.settings.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${prefix}@${defaultEmailDomain}` : prefix;
|
||||
@@ -538,7 +545,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
...prev,
|
||||
'login.email': email
|
||||
}));
|
||||
}, [dbContext?.sqliteClient, fieldValues]);
|
||||
}, [dbContext?.sqliteClient, fieldValues, generateRandomEmailPrefix]);
|
||||
|
||||
/**
|
||||
* Generate a random-string email alias (for Login type email field).
|
||||
|
||||
@@ -395,18 +395,26 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
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: ''
|
||||
});
|
||||
let prefix: string;
|
||||
|
||||
if (!firstName.trim() && !lastName.trim()) {
|
||||
// No alias identity fields filled in, fall back to random prefix.
|
||||
prefix = generator.generateRandomEmailPrefix();
|
||||
} else {
|
||||
const gender = (fieldValues['alias.gender'] as string) || Gender.Other;
|
||||
const birthdate = (fieldValues['alias.birthdate'] as string) || '';
|
||||
|
||||
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;
|
||||
|
||||
@@ -272,29 +272,20 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
onChange('');
|
||||
setLocalPart('');
|
||||
} else {
|
||||
// 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
|
||||
// Switching to domain chooser mode - clear old email-mode value.
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: PUBLIC_EMAIL_DOMAINS[0];
|
||||
setSelectedDomain(defaultDomain);
|
||||
setLocalPart('');
|
||||
onChange('');
|
||||
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
setLocalPart(value);
|
||||
if (onGenerateAlias) {
|
||||
// Delegate to the parent callback which sets the full email value (prefix@domain)
|
||||
onGenerateAlias();
|
||||
}
|
||||
}
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange, onGenerateAlias]);
|
||||
}, [isCustomDomain, showPrivateDomains, privateEmailDomains, onChange, onGenerateAlias]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
domainAt: {
|
||||
@@ -524,8 +515,6 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
style={styles.textInput}
|
||||
value={isCustomDomain ? value : localPart}
|
||||
onChangeText={handleLocalPartChange}
|
||||
placeholder={isCustomDomain ? t('items.enterFullEmail') : t('items.enterEmailPrefix')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
|
||||
@@ -177,8 +177,6 @@ export const ItemNameField = forwardRef<ItemNameFieldRef, IItemNameFieldProps>((
|
||||
style={styles.input}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={t('items.itemName')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
testID="item-name-input"
|
||||
/>
|
||||
{hasFolders && (
|
||||
|
||||
@@ -405,7 +405,6 @@
|
||||
"manual": "Manual",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
|
||||
@@ -304,6 +304,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
ResetToDefaultDomain();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces SelectedDomain to the first available domain.
|
||||
/// Used when switching to alias mode to clear any stale domain from email mode.
|
||||
/// </summary>
|
||||
private void ResetToDefaultDomain()
|
||||
{
|
||||
if (ShowPrivateDomains && PrivateDomains.Count > 0)
|
||||
{
|
||||
SelectedDomain = PrivateDomains[0];
|
||||
@@ -430,7 +439,7 @@
|
||||
|
||||
ModeToggledByUser = true;
|
||||
IsCustomDomain = false;
|
||||
EnsureDefaultDomain();
|
||||
ResetToDefaultDomain();
|
||||
|
||||
// Clear old email-mode value so it doesn't interfere with alias mode.
|
||||
LocalPart = string.Empty;
|
||||
@@ -445,10 +454,12 @@
|
||||
|
||||
/// <summary>
|
||||
/// Handles the regenerate button click.
|
||||
/// Delegates to the parent callback to generate a new email value.
|
||||
/// Locks alias mode so auto-detection doesn't override it, then delegates
|
||||
/// to the parent callback to generate a new email value.
|
||||
/// </summary>
|
||||
private async Task HandleRegenerate()
|
||||
{
|
||||
ModeToggledByUser = true;
|
||||
if (OnGenerateAlias.HasDelegate)
|
||||
{
|
||||
await OnGenerateAlias.InvokeAsync();
|
||||
|
||||
@@ -1108,16 +1108,29 @@ else
|
||||
/// </summary>
|
||||
private async Task HandleGenerateAliasEmail()
|
||||
{
|
||||
var identity = new AliasVaultIdentity
|
||||
{
|
||||
FirstName = Obj.GetFieldValue(FieldKey.AliasFirstName),
|
||||
LastName = Obj.GetFieldValue(FieldKey.AliasLastName),
|
||||
BirthDate = Obj.GetFieldValue(FieldKey.AliasBirthdate),
|
||||
Gender = Obj.GetFieldValue(FieldKey.AliasGender),
|
||||
NickName = Obj.GetFieldValue(FieldKey.LoginUsername),
|
||||
};
|
||||
var firstName = Obj.GetFieldValue(FieldKey.AliasFirstName);
|
||||
var lastName = Obj.GetFieldValue(FieldKey.AliasLastName);
|
||||
|
||||
string prefix;
|
||||
if (string.IsNullOrWhiteSpace(firstName) && string.IsNullOrWhiteSpace(lastName))
|
||||
{
|
||||
// No alias identity fields filled in, fall back to random prefix.
|
||||
prefix = await JsInteropService.GenerateRandomStringEmailPrefixAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var identity = new AliasVaultIdentity
|
||||
{
|
||||
FirstName = firstName,
|
||||
LastName = lastName,
|
||||
BirthDate = Obj.GetFieldValue(FieldKey.AliasBirthdate),
|
||||
Gender = Obj.GetFieldValue(FieldKey.AliasGender),
|
||||
NickName = Obj.GetFieldValue(FieldKey.LoginUsername),
|
||||
};
|
||||
|
||||
prefix = await JsInteropService.GenerateRandomEmailPrefixAsync(identity);
|
||||
}
|
||||
|
||||
var prefix = await JsInteropService.GenerateRandomEmailPrefixAsync(identity);
|
||||
var defaultEmailDomain = DbService.Settings.DefaultEmailDomain;
|
||||
var email = !string.IsNullOrEmpty(defaultEmailDomain) ? $"{prefix}@{defaultEmailDomain}" : prefix;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user