Add identity generator options to browser extension settings (#1571)

This commit is contained in:
Leendert de Borst
2026-02-01 11:18:28 +00:00
committed by Leendert de Borst
parent 63bc50d862
commit 75d74d46b1
5 changed files with 341 additions and 95 deletions
@@ -31,6 +31,7 @@ import AutofillSettings from '@/entrypoints/popup/pages/settings/AutofillSetting
import AutoLockSettings from '@/entrypoints/popup/pages/settings/AutoLockSettings';
import ClipboardSettings from '@/entrypoints/popup/pages/settings/ClipboardSettings';
import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuSettings';
import IdentityGeneratorSettings from '@/entrypoints/popup/pages/settings/IdentityGeneratorSettings';
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
import PasskeySettings from '@/entrypoints/popup/pages/settings/PasskeySettings';
import Settings from '@/entrypoints/popup/pages/settings/Settings';
@@ -203,6 +204,7 @@ const App: React.FC = () => {
{ path: '/settings/language', element: <LanguageSettings />, showBackButton: true, title: t('settings.language') },
{ path: '/settings/auto-lock', element: <AutoLockSettings />, showBackButton: true, title: t('settings.autoLockTimeout') },
{ path: '/settings/passkeys', element: <PasskeySettings />, showBackButton: true, title: t('settings.passkeySettings') },
{ path: '/settings/identity-generator', element: <IdentityGeneratorSettings />, showBackButton: true, title: t('settings.identityGenerator') },
], [t]);
useEffect(() => {
@@ -1,5 +1,5 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import React, { useRef, useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { ClipboardCountdownBar } from '@/entrypoints/popup/components/ClipboardCountdownBar';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
@@ -30,6 +30,16 @@ type DefaultLayoutProps = {
* This is the main layout used for most pages in the extension.
*/
const DefaultLayout: React.FC<DefaultLayoutProps> = ({ routes, headerButtons, message, children }) => {
const mainRef = useRef<HTMLElement>(null);
const location = useLocation();
// Reset scroll position when route changes
useEffect(() => {
if (mainRef.current) {
mainRef.current.scrollTop = 0;
}
}, [location.pathname]);
return (
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
<ClipboardCountdownBar />
@@ -40,6 +50,7 @@ const DefaultLayout: React.FC<DefaultLayoutProps> = ({ routes, headerButtons, me
/>
<main
ref={mainRef}
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
@@ -0,0 +1,179 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { getAvailableLanguages, getAvailableAgeRanges, ILanguageOption, IAgeRangeOption } from '@/utils/dist/core/identity-generator';
/**
* Identity Generator Settings page component.
* Allows users to configure default language, gender, and age range for identity generation.
*/
const IdentityGeneratorSettings: React.FC = () => {
const { t } = useTranslation();
const { setIsInitialLoading } = useLoading();
const dbContext = useDb();
const { executeVaultMutationAsync } = useVaultMutate();
const [language, setLanguage] = useState<string>('en');
const [gender, setGender] = useState<string>('random');
const [ageRange, setAgeRange] = useState<string>('random');
const [languageOptions, setLanguageOptions] = useState<ILanguageOption[]>([]);
const [ageRangeOptions, setAgeRangeOptions] = useState<IAgeRangeOption[]>([]);
const GENDER_OPTIONS = [
{ label: t('settings.identityGeneratorSettings.genderOptions.random'), value: 'random' },
{ label: t('settings.identityGeneratorSettings.genderOptions.male'), value: 'male' },
{ label: t('settings.identityGeneratorSettings.genderOptions.female'), value: 'female' }
];
/**
* Load settings on mount.
*/
useEffect(() => {
const languages = getAvailableLanguages();
const ranges = getAvailableAgeRanges();
setLanguageOptions(languages);
setAgeRangeOptions(ranges);
if (dbContext?.sqliteClient) {
const currentLanguage = dbContext.sqliteClient.settings.getEffectiveIdentityLanguage();
const currentGender = dbContext.sqliteClient.settings.getDefaultIdentityGender();
const currentAgeRange = dbContext.sqliteClient.settings.getDefaultIdentityAgeRange();
setLanguage(currentLanguage);
setGender(currentGender);
setAgeRange(currentAgeRange);
}
setIsInitialLoading(false);
}, [dbContext?.sqliteClient, setIsInitialLoading]);
/**
* Handle language change.
*/
const handleLanguageChange = useCallback(async (newLanguage: string): Promise<void> => {
setLanguage(newLanguage);
if (dbContext?.sqliteClient) {
await executeVaultMutationAsync(async () => {
dbContext.sqliteClient!.settings.updateSetting('DefaultIdentityLanguage', newLanguage);
});
}
}, [dbContext?.sqliteClient, executeVaultMutationAsync]);
/**
* Handle gender change.
*/
const handleGenderChange = useCallback(async (newGender: string): Promise<void> => {
setGender(newGender);
if (dbContext?.sqliteClient) {
await executeVaultMutationAsync(async () => {
dbContext.sqliteClient!.settings.updateSetting('DefaultIdentityGender', newGender);
});
}
}, [dbContext?.sqliteClient, executeVaultMutationAsync]);
/**
* Handle age range change.
*/
const handleAgeRangeChange = useCallback(async (newAgeRange: string): Promise<void> => {
setAgeRange(newAgeRange);
if (dbContext?.sqliteClient) {
await executeVaultMutationAsync(async () => {
dbContext.sqliteClient!.settings.updateSetting('DefaultIdentityAgeRange', newAgeRange);
});
}
}, [dbContext?.sqliteClient, executeVaultMutationAsync]);
return (
<div className="space-y-6">
{/* Language Section */}
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">
{t('settings.identityGeneratorSettings.languageSection')}
</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<p className="font-medium text-gray-900 dark:text-white mb-2">
{t('settings.identityGeneratorSettings.languageSection')}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t('settings.identityGeneratorSettings.languageDescription')}
</p>
<select
value={language}
onChange={(e) => handleLanguageChange(e.target.value)}
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
>
{languageOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.flag} {option.label}
</option>
))}
</select>
</div>
</div>
</section>
{/* Gender Section */}
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">
{t('settings.identityGeneratorSettings.genderSection')}
</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<p className="font-medium text-gray-900 dark:text-white mb-2">
{t('settings.identityGeneratorSettings.genderSection')}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t('settings.identityGeneratorSettings.genderDescription')}
</p>
<select
value={gender}
onChange={(e) => handleGenderChange(e.target.value)}
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
>
{GENDER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</section>
{/* Age Range Section */}
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">
{t('settings.identityGeneratorSettings.ageRangeSection')}
</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<p className="font-medium text-gray-900 dark:text-white mb-2">
{t('settings.identityGeneratorSettings.ageRangeSection')}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t('settings.identityGeneratorSettings.ageRangeDescription')}
</p>
<select
value={ageRange}
onChange={(e) => handleAgeRangeChange(e.target.value)}
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
>
{ageRangeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.value === 'random' ? t('settings.identityGeneratorSettings.genderOptions.random') : option.label}
</option>
))}
</select>
</div>
</div>
</section>
</div>
);
};
export default IdentityGeneratorSettings;
@@ -192,6 +192,13 @@ const Settings: React.FC = () => {
navigate('/settings/passkeys');
};
/**
* Navigate to identity generator settings.
*/
const navigateToIdentityGeneratorSettings = () : void => {
navigate('/settings/identity-generator');
};
return (
<>
{/* Logout Confirmation Modal */}
@@ -279,60 +286,6 @@ const Settings: React.FC = () => {
<section>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{/* Vault Unlock Method */}
<button
onClick={navigateToUnlockMethodSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.unlockMethod.title')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Auto-lock Settings */}
<button
onClick={navigateToAutoLockSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.autoLockTimeout')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Autofill Settings */}
<button
onClick={navigateToAutofillSettings}
@@ -396,14 +349,61 @@ const Settings: React.FC = () => {
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</section>
{/* Additional Settings Section */}
<section>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{/* Vault Unlock Method */}
<button
onClick={navigateToUnlockMethodSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.unlockMethod.title')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Auto-lock Settings */}
<button
onClick={navigateToAutoLockSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.autoLockTimeout')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Clipboard Settings */}
<button
onClick={navigateToClipboardSettings}
@@ -436,38 +436,6 @@ const Settings: React.FC = () => {
</svg>
</button>
{/* Context Menu Settings */}
<button
onClick={navigateToContextMenuSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.contextMenuSettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Language Settings */}
<button
onClick={navigateToLanguageSettings}
@@ -503,6 +471,77 @@ const Settings: React.FC = () => {
</div>
</section>
{/* Generator Settings Section */}
<section>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{/* Identity Generator Settings */}
<button
onClick={navigateToIdentityGeneratorSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.identityGenerator')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Context Menu Settings */}
<button
onClick={navigateToContextMenuSettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
<span className="text-gray-900 dark:text-white text-left">{t('settings.contextMenuSettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</section>
{/* Appearance Settings Section */}
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
@@ -403,6 +403,21 @@
"clientUrlRequired": "Client URL is required",
"clientUrlInvalid": "Please enter a valid client URL"
},
"identityGenerator": "Identity Generator",
"identityGeneratorSettings": {
"description": "Configure the default language and gender preference for generating new identities.",
"languageSection": "Language",
"languageDescription": "Set the language that will be used when generating new identities.",
"genderSection": "Gender",
"genderDescription": "Set the gender preference for generating new identities.",
"ageRangeSection": "Age Range",
"ageRangeDescription": "Set the age range for generating new identities.",
"genderOptions": {
"random": "Random",
"male": "Male",
"female": "Female"
}
},
"unlockMethod": {
"title": "Vault Unlock Method",
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",