aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:24 +0200
commit51d2d734c228f1e46670962480258abfe586d6c4 (patch)
tree12e869044c80c889f83b588210cc0ac500cd7a6a /packages/meshbay-hub/src/meshbay_hub/static
parentf4c6628c8e85513d9fd110ead95682368a15a0fd (diff)
parentc6fd7ea89b6e0a96eb1d81989de891b4768b1044 (diff)
downloadmeshbay-51d2d734c228f1e46670962480258abfe586d6c4.tar.gz
Merge branch 'feat/email-verification'
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js86
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js20
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js29
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js27
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js29
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js28
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js29
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js29
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js33
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js36
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js35
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js34
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/profile-page.js71
13 files changed, 389 insertions, 97 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index 1cca56f..860f890 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -68,16 +68,23 @@ export function LoginPage({ onLogin }) {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
+ const [pendingVerif, setPendingVerif] = useState(false);
+
const onSubmit = async (e) => {
e.preventDefault();
if (!username || !password) return;
setError('');
+ setPendingVerif(false);
setLoading(true);
try {
await onLogin(username, password);
navigate('/');
} catch (err) {
- setError(err.message);
+ if (err.message === 'email_verification_required') {
+ setPendingVerif(true);
+ } else {
+ setError(err.message);
+ }
} finally {
setLoading(false);
}
@@ -95,6 +102,11 @@ export function LoginPage({ onLogin }) {
onInput=${e => setPassword(e.target.value)}
autocomplete="current-password" required />
${error && html`<div class="error-msg">${error}</div>`}
+ ${pendingVerif && html`
+ <div class="error-msg" style="background:var(--bg-secondary);border-left:3px solid var(--yellow, #f59e0b)">
+ <p>${t('login.pending_verification')}</p>
+ </div>
+ `}
<button type="submit" disabled=${loading}>
${loading ? t('login.loading') : t('login.submit')}
</button>
@@ -113,8 +125,11 @@ export function RegisterPage() {
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState('');
- const [success, setSuccess] = useState(false);
+ const [phase, setPhase] = useState('form'); // form | verify | done
const [loading, setLoading] = useState(false);
+ const [code, setCode] = useState('');
+ const [verifying, setVerifying] = useState(false);
+ const [resent, setResent] = useState(false);
const onSubmit = async (e) => {
e.preventDefault();
@@ -136,7 +151,7 @@ export function RegisterPage() {
body: { username, email, password, pk_user_ed25519: '', pk_user_x25519: '' },
});
}
- setSuccess(true);
+ setPhase('verify');
} catch (err) {
setError(err.message);
} finally {
@@ -144,7 +159,53 @@ export function RegisterPage() {
}
};
- if (success) {
+ const onVerify = async (e) => {
+ e.preventDefault();
+ if (!code.trim()) return;
+ setError('');
+ setVerifying(true);
+ try {
+ await hubFetch('/v1/users/verify-email', {
+ method: 'POST',
+ body: { email, code: code.trim() },
+ });
+ setPhase('done');
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setVerifying(false);
+ }
+ };
+
+ const onResend = async () => {
+ setError('');
+ setResent(false);
+ try {
+ await hubFetch('/v1/users/register', {
+ method: 'POST',
+ body: { username, email, password, pk_user_ed25519: '', pk_user_x25519: '' },
+ });
+ setResent(true);
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ if (phase === 'done') {
+ return html`
+ <div class="page-center">
+ <div class="card login-card">
+ <h2>${t('register.verified_title')}</h2>
+ <p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)">
+ ${t('register.verified_msg')}
+ </p>
+ <a href="#/login" style="display:block; text-align:center">${t('register.go_login')}</a>
+ </div>
+ </div>
+ `;
+ }
+
+ if (phase === 'verify') {
return html`
<div class="page-center">
<div class="card login-card">
@@ -152,7 +213,22 @@ export function RegisterPage() {
<p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)">
${t('register.success_msg')}
</p>
- <a href="#/login" style="display:block; text-align:center">${t('register.go_login')}</a>
+ <form onSubmit=${onVerify}>
+ <input type="text" placeholder="${t('register.code_placeholder')}"
+ value=${code} onInput=${e => setCode(e.target.value)}
+ autocomplete="one-time-code" inputmode="numeric"
+ maxlength="6" required autofocus
+ style="text-align:center;font-size:1.4em;letter-spacing:0.3em" />
+ ${error && html`<div class="error-msg">${error}</div>`}
+ <button type="submit" disabled=${verifying}>
+ ${verifying ? t('register.verifying') : t('register.verify_btn')}
+ </button>
+ </form>
+ <div class="login-footer">
+ <button class="link-btn" onClick=${onResend}>${t('register.resend')}</button>
+ ${resent && html`<span style="color:var(--success);margin-left:8px">
+ ${t('register.resend_sent')}</span>`}
+ </div>
</div>
</div>
`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index 4f55dfb..1c6ca71 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -793,7 +793,21 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
method: 'POST', token, body: {},
});
- setInviteCode({ username, code: result.code, expires: result.expires_at });
+ // Send an email notification to the invitee with the code.
+ // The hub decrypts their email server-side — the inviter never sees it.
+ let emailStatus = 'no_email';
+ try {
+ const notif = await hubFetch(`/v1/groups/${groupId}/invite-notify`, {
+ method: 'POST', token,
+ body: { username, code: result.code, group_name: group?.name || '' },
+ });
+ emailStatus = notif.status;
+ } catch { /* best effort */ }
+
+ setInviteCode({
+ username, code: result.code, expires: result.expires_at,
+ emailSent: emailStatus === 'sent',
+ });
setInviteUser('');
loadMembers();
} catch (err) {
@@ -831,6 +845,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
<div class="success-msg" style="margin-bottom:8px">
<p>${t('members.invite_code_ready', { user: inviteCode.username })}</p>
<p class="code-display">${inviteCode.code}</p>
+ ${inviteCode.emailSent
+ ? html`<p style="color:var(--success)">${t('members.invite_email_sent')}</p>`
+ : html`<p style="color:var(--text-dim)">${t('members.invite_email_failed')}</p>`
+ }
<p>${t('members.invite_code_hint')}</p>
</div>
`}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 8284527..909fec5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -40,9 +40,18 @@ export default {
'register.loading': 'Konto wird erstellt …',
'register.has_account': 'Sie haben bereits ein Konto?',
'register.login_link': 'Anmelden',
- 'register.success_title': 'Konto erstellt',
- 'register.success_msg': 'Sie können sich jetzt mit Ihren Zugangsdaten anmelden.',
+ 'register.success_title': 'E-Mail bestätigen',
+ 'register.success_msg': 'Ein Bestätigungscode wurde an Ihre E-Mail-Adresse gesendet. Geben Sie ihn unten ein, um Ihr Konto zu aktivieren.',
'register.go_login': 'Zur Anmeldung',
+ 'register.code_placeholder': 'Bestätigungscode',
+ 'register.verify_btn': 'Bestätigen',
+ 'register.verifying': 'Überprüfung…',
+ 'register.verified_title': 'E-Mail bestätigt',
+ 'register.verified_msg': 'Ihr Konto ist aktiv. Sie können sich jetzt anmelden.',
+ 'register.resend': 'Code erneut senden',
+ 'register.resend_sent': 'Ein neuer Code wurde gesendet.',
+ 'login.pending_verification': 'Ihre E-Mail-Adresse ist noch nicht bestätigt. Bitte prüfen Sie Ihr Postfach auf den Bestätigungscode.',
+ 'login.verify_link': 'E-Mail bestätigen',
'register.err_mismatch': 'Die Passwörter stimmen nicht überein',
'register.err_min_len': {
one: 'Verwenden Sie mindestens {n} Zeichen',
@@ -262,7 +271,12 @@ export default {
'settings.username': 'Benutzername',
'settings.email': 'Email',
'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
+ 'settings.email_saved': 'E-Mail aktualisiert',
+ 'settings.email_code_sent': 'Ein Bestätigungscode wurde an Ihre neue E-Mail-Adresse gesendet.',
+ 'settings.email_code_placeholder': 'Bestätigungscode',
+ 'settings.email_verify_btn': 'Bestätigen',
+ 'settings.email_verifying': 'Überprüfung…',
+ 'settings.email_verified': 'E-Mail-Adresse erfolgreich geändert.',
'settings.notif_global_disable': 'Disable all notifications',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'Standardwerte',
@@ -468,10 +482,11 @@ export default {
+ 'Code läuft nie über den Hub.',
'members.pair_btn': 'Koppeln',
'members.pair_success': 'Dieser Browser ist jetzt mit dem Node gekoppelt.',
- 'members.invite_code_ready': 'Einladungscode für {user} — schicken Sie ihn auf dem '
- + 'Weg, den Sie ohnehin nutzen. Er funktioniert einmal und läuft nie über den Hub.',
- 'members.invite_code_hint': 'Die eingeladene Person gibt ihn ein, wenn sie diese '
- + 'Gruppe zum ersten Mal öffnet. Sie müssen dann nicht online sein.',
+ 'members.invite_code_ready': 'Einladungscode für {user}:',
+ 'members.invite_email_sent': 'Eine E-Mail mit dem Code wurde an dieses Mitglied gesendet.',
+ 'members.invite_email_failed': 'Die E-Mail konnte nicht gesendet werden — bitte teilen Sie den Code manuell mit.',
+ 'members.invite_code_hint': 'Sie können diesen Code auch über einen anderen Kanal teilen (z. B. SMS). '
+ + 'Die eingeladene Person gibt ihn ein, wenn sie diese Gruppe zum ersten Mal öffnet.',
'transfers.title': 'Übertragungen',
'transfers.clear': 'Abgeschlossene entfernen',
'transfers.cancel': 'Abbrechen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index b98e032..a6f77b3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -43,9 +43,18 @@ export default {
'register.loading': 'Creating account...',
'register.has_account': 'Already have an account?',
'register.login_link': 'Login',
- 'register.success_title': 'Account created',
- 'register.success_msg': 'You can now log in with your credentials.',
+ 'register.success_title': 'Verify your email',
+ 'register.success_msg': 'A verification code has been sent to your email address. Enter it below to activate your account.',
'register.go_login': 'Go to login',
+ 'register.code_placeholder': 'Verification code',
+ 'register.verify_btn': 'Verify',
+ 'register.verifying': 'Verifying...',
+ 'register.verified_title': 'Email verified',
+ 'register.verified_msg': 'Your account is active. You can now log in.',
+ 'register.resend': 'Resend code',
+ 'register.resend_sent': 'A new code has been sent.',
+ 'login.pending_verification': 'Your email is not yet verified. Please check your inbox for the verification code.',
+ 'login.verify_link': 'Verify email',
'register.err_mismatch': 'Passwords do not match',
'register.err_min_len': {
one: 'Use at least {n} character',
@@ -260,6 +269,11 @@ export default {
'settings.email': 'Email',
'settings.email_save': 'Save',
'settings.email_saved': 'Email updated',
+ 'settings.email_code_sent': 'A verification code has been sent to your new email address.',
+ 'settings.email_code_placeholder': 'Verification code',
+ 'settings.email_verify_btn': 'Verify',
+ 'settings.email_verifying': 'Verifying...',
+ 'settings.email_verified': 'Email address changed successfully.',
'settings.notif_global_disable': 'Disable all notifications',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'Defaults',
@@ -547,10 +561,11 @@ export default {
+ 'never passes through the hub.',
'members.pair_btn': 'Pair',
'members.pair_success': 'This browser is now paired with the node.',
- 'members.invite_code_ready': 'Invitation code for {user} — send it to them the way '
- + 'you normally talk. It works once, and it never passes through the hub.',
- 'members.invite_code_hint': 'They enter it the first time they open this group. '
- + 'You do not need to be online then.',
+ 'members.invite_code_ready': 'Invitation code for {user}:',
+ 'members.invite_email_sent': 'An email with the code has been sent to this member.',
+ 'members.invite_email_failed': 'Could not send the email — share the code manually.',
+ 'members.invite_code_hint': 'You can also share this code via another channel (e.g. SMS). '
+ + 'They enter it the first time they open this group.',
'transfers.title': 'Transfers',
'transfers.clear': 'Clear finished',
'transfers.cancel': 'Cancel',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index d0a7004..640eb91 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -39,9 +39,18 @@ export default {
'register.loading': 'Creando la cuenta...',
'register.has_account': '¿Ya tiene una cuenta?',
'register.login_link': 'Iniciar sesión',
- 'register.success_title': 'Cuenta creada',
- 'register.success_msg': 'Ya puede iniciar sesión con sus credenciales.',
+ 'register.success_title': 'Verifique su correo',
+ 'register.success_msg': 'Se ha enviado un código de verificación a su dirección de correo electrónico. Introdúzcalo a continuación para activar su cuenta.',
'register.go_login': 'Ir al inicio de sesión',
+ 'register.code_placeholder': 'Código de verificación',
+ 'register.verify_btn': 'Verificar',
+ 'register.verifying': 'Verificando…',
+ 'register.verified_title': 'Correo verificado',
+ 'register.verified_msg': 'Su cuenta está activa. Ya puede iniciar sesión.',
+ 'register.resend': 'Reenviar código',
+ 'register.resend_sent': 'Se ha enviado un nuevo código.',
+ 'login.pending_verification': 'Su correo electrónico aún no ha sido verificado. Revise su bandeja de entrada para obtener el código de verificación.',
+ 'login.verify_link': 'Verificar correo',
'register.err_mismatch': 'Las contraseñas no coinciden',
'register.err_min_len': {
one: 'Use al menos {n} carácter',
@@ -260,7 +269,12 @@ export default {
'settings.username': 'Nombre de usuario',
'settings.email': 'Email',
'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
+ 'settings.email_saved': 'Correo actualizado',
+ 'settings.email_code_sent': 'Se ha enviado un código de verificación a su nueva dirección de correo.',
+ 'settings.email_code_placeholder': 'Código de verificación',
+ 'settings.email_verify_btn': 'Verificar',
+ 'settings.email_verifying': 'Verificando…',
+ 'settings.email_verified': 'Dirección de correo cambiada con éxito.',
'settings.notif_global_disable': 'Disable all notifications',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'Valores predeterminados',
@@ -464,10 +478,11 @@ export default {
+ 'código nunca pasa por el hub.',
'members.pair_btn': 'Emparejar',
'members.pair_success': 'Este navegador ya está emparejado con el node.',
- 'members.invite_code_ready': 'Código de invitación para {user} — envíeselo por el '
- + 'medio que usen habitualmente. Funciona una sola vez y nunca pasa por el hub.',
- 'members.invite_code_hint': 'Lo introduce la primera vez que abre este grupo. No '
- + 'necesita que usted esté en línea en ese momento.',
+ 'members.invite_code_ready': 'Código de invitación para {user}:',
+ 'members.invite_email_sent': 'Se ha enviado un correo con el código a este miembro.',
+ 'members.invite_email_failed': 'No se pudo enviar el correo — comparta el código manualmente.',
+ 'members.invite_code_hint': 'También puede compartir este código por otro canal (ej. SMS). '
+ + 'Lo introduce la primera vez que abre este grupo.',
'transfers.title': 'Transferencias',
'transfers.clear': 'Borrar las terminadas',
'transfers.cancel': 'Cancelar',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 1e7bb79..de6b7c8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -39,9 +39,18 @@ export default {
'register.loading': 'Création du compte...',
'register.has_account': 'Vous avez déjà un compte ?',
'register.login_link': 'Se connecter',
- 'register.success_title': 'Compte créé',
- 'register.success_msg': 'Vous pouvez maintenant vous connecter avec vos identifiants.',
+ 'register.success_title': 'Vérifiez votre e-mail',
+ 'register.success_msg': 'Un code de vérification a été envoyé à votre adresse e-mail. Saisissez-le ci-dessous pour activer votre compte.',
'register.go_login': 'Aller à la connexion',
+ 'register.code_placeholder': 'Code de vérification',
+ 'register.verify_btn': 'Vérifier',
+ 'register.verifying': 'Vérification...',
+ 'register.verified_title': 'E-mail vérifié',
+ 'register.verified_msg': 'Votre compte est actif. Vous pouvez maintenant vous connecter.',
+ 'register.resend': 'Renvoyer le code',
+ 'register.resend_sent': 'Un nouveau code a été envoyé.',
+ 'login.pending_verification': 'Votre e-mail n\'est pas encore vérifié. Consultez votre boîte de réception pour le code de vérification.',
+ 'login.verify_link': 'Vérifier l\'e-mail',
'register.err_mismatch': 'Les mots de passe ne correspondent pas',
'register.err_min_len': {
one: 'Utilisez au moins {n} caractère',
@@ -262,6 +271,11 @@ export default {
'settings.email': 'E-mail',
'settings.email_save': 'Enregistrer',
'settings.email_saved': 'E-mail mis à jour',
+ 'settings.email_code_sent': 'Un code de vérification a été envoyé à votre nouvelle adresse e-mail.',
+ 'settings.email_code_placeholder': 'Code de vérification',
+ 'settings.email_verify_btn': 'Vérifier',
+ 'settings.email_verifying': 'Vérification...',
+ 'settings.email_verified': 'Adresse e-mail modifiée avec succès.',
'settings.notif_global_disable': 'Désactiver toutes les notifications',
'settings.notif_global_hint': 'Quand activé, aucune notification n\x27est créée pour aucun groupe.',
'settings.defaults': 'Valeurs par défaut',
@@ -467,11 +481,11 @@ export default {
+ 'ici. Le code ne transite jamais par le hub.',
'members.pair_btn': 'Appairer',
'members.pair_success': 'Ce navigateur est désormais appairé avec le node.',
- 'members.invite_code_ready': 'Code d’invitation pour {user} — transmettez-le-lui '
- + 'par le moyen dont vous avez l’habitude. Il ne fonctionne qu’une fois, et ne '
- + 'transite jamais par le hub.',
- 'members.invite_code_hint': 'La personne le saisit la première fois qu’elle ouvre '
- + 'ce groupe. Vous n’avez pas besoin d’être en ligne à ce moment-là.',
+ 'members.invite_code_ready': "Code d’invitation pour {user} :",
+ 'members.invite_email_sent': "Un e-mail avec le code a été envoyé à ce membre.",
+ 'members.invite_email_failed': "Impossible d’envoyer l’e-mail — partagez le code manuellement.",
+ 'members.invite_code_hint': "Vous pouvez aussi transmettre ce code par un autre canal (ex. SMS). "
+ + "La personne le saisit la première fois qu’elle ouvre ce groupe.",
'transfers.title': 'Transferts',
'transfers.clear': 'Effacer les terminés',
'transfers.cancel': 'Annuler',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index eec9b65..52f6279 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -40,9 +40,18 @@ export default {
'register.loading': "Creazione dell'account...",
'register.has_account': 'Ha già un account?',
'register.login_link': 'Accedi',
- 'register.success_title': 'Account creato',
- 'register.success_msg': 'Ora può accedere con le sue credenziali.',
+ 'register.success_title': 'Verifichi la sua e-mail',
+ 'register.success_msg': 'Un codice di verifica è stato inviato al suo indirizzo e-mail. Lo inserisca qui sotto per attivare il suo account.',
'register.go_login': "Vai all'accesso",
+ 'register.code_placeholder': 'Codice di verifica',
+ 'register.verify_btn': 'Verifica',
+ 'register.verifying': 'Verifica in corso…',
+ 'register.verified_title': 'E-mail verificata',
+ 'register.verified_msg': 'Il suo account è attivo. Ora può accedere.',
+ 'register.resend': 'Invia di nuovo il codice',
+ 'register.resend_sent': 'Un nuovo codice è stato inviato.',
+ 'login.pending_verification': 'Il suo indirizzo e-mail non è ancora verificato. Controlli la sua casella di posta per il codice di verifica.',
+ 'login.verify_link': 'Verifica e-mail',
'register.err_mismatch': 'Le password non coincidono',
'register.err_min_len': {
one: 'Usi almeno {n} carattere',
@@ -261,7 +270,12 @@ export default {
'settings.username': 'Nome utente',
'settings.email': 'Email',
'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
+ 'settings.email_saved': 'E-mail aggiornata',
+ 'settings.email_code_sent': 'Un codice di verifica è stato inviato al suo nuovo indirizzo e-mail.',
+ 'settings.email_code_placeholder': 'Codice di verifica',
+ 'settings.email_verify_btn': 'Verifica',
+ 'settings.email_verifying': 'Verifica in corso…',
+ 'settings.email_verified': 'Indirizzo e-mail modificato con successo.',
'settings.notif_global_disable': 'Disable all notifications',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'Valori predefiniti',
@@ -467,10 +481,11 @@ export default {
+ 'passa mai per l’hub.',
'members.pair_btn': 'Associa',
'members.pair_success': 'Questo browser è ora associato al node.',
- 'members.invite_code_ready': 'Codice di invito per {user} — glielo mandi con il '
- + 'mezzo che usate di solito. Funziona una volta sola e non passa mai per l’hub.',
- 'members.invite_code_hint': 'Lo inserirà la prima volta che aprirà questo gruppo. '
- + 'Non è necessario che lei sia online in quel momento.',
+ 'members.invite_code_ready': 'Codice di invito per {user}:',
+ 'members.invite_email_sent': "Un'e-mail con il codice è stata inviata a questo membro.",
+ 'members.invite_email_failed': "Impossibile inviare l'e-mail — condivida il codice manualmente.",
+ 'members.invite_code_hint': 'Può anche condividere questo codice tramite un altro canale (es. SMS). '
+ + 'Lo inserirà la prima volta che aprirà questo gruppo.',
'transfers.title': 'Trasferimenti',
'transfers.clear': 'Rimuovi i completati',
'transfers.cancel': 'Annulla',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 023ca33..75880fa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -40,9 +40,18 @@ export default {
'register.loading': 'アカウントを作成しています…',
'register.has_account': 'すでにアカウントをお持ちですか?',
'register.login_link': 'ログイン',
- 'register.success_title': 'アカウントを作成しました',
- 'register.success_msg': 'ご自身の資格情報でログインできるようになりました。',
+ 'register.success_title': 'メールアドレスを確認してください',
+ 'register.success_msg': '確認コードをメールアドレスに送信しました。以下に入力してアカウントを有効にしてください。',
'register.go_login': 'ログインへ進む',
+ 'register.code_placeholder': '確認コード',
+ 'register.verify_btn': '確認',
+ 'register.verifying': '確認中…',
+ 'register.verified_title': 'メール確認完了',
+ 'register.verified_msg': 'アカウントが有効になりました。ログインできます。',
+ 'register.resend': 'コードを再送信',
+ 'register.resend_sent': '新しいコードを送信しました。',
+ 'login.pending_verification': 'メールアドレスがまだ確認されていません。受信トレイで確認コードをご確認ください。',
+ 'login.verify_link': 'メールを確認',
'register.err_mismatch': 'パスワードが一致しません',
'register.err_min_len': {
other: '{n} 文字以上でご入力ください',
@@ -258,7 +267,12 @@ export default {
'settings.username': 'ユーザー名',
'settings.email': 'Email',
'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
+ 'settings.email_saved': 'メールアドレスを更新しました',
+ 'settings.email_code_sent': '新しいメールアドレスに確認コードを送信しました。',
+ 'settings.email_code_placeholder': '確認コード',
+ 'settings.email_verify_btn': '確認',
+ 'settings.email_verifying': '確認中…',
+ 'settings.email_verified': 'メールアドレスが正常に変更されました。',
'settings.notif_global_disable': 'Disable all notifications',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'デフォルト',
@@ -459,10 +473,11 @@ export default {
+ 'このコードが hub を経由することはありません。',
'members.pair_btn': 'ペアリング',
'members.pair_success': 'このブラウザーは node とペアリングされました。',
- 'members.invite_code_ready': '{user} 宛ての招待コードです。'
- + 'いつもの連絡手段でお送りください。使えるのは 1 回だけで、hub を経由することはありません。',
- 'members.invite_code_hint': '相手がこのグループを初めて開いたときに入力します。'
- + 'そのときお客様がオンラインである必要はありません。',
+ 'members.invite_code_ready': '{user} 宛ての招待コード:',
+ 'members.invite_email_sent': 'このメンバーにコード付きのメールが送信されました。',
+ 'members.invite_email_failed': 'メールを送信できませんでした。コードを手動で共有してください。',
+ 'members.invite_code_hint': 'このコードは別の手段(SMSなど)でも共有できます。'
+ + '相手がこのグループを初めて開いたときに入力します。',
'transfers.title': '転送',
'transfers.clear': '完了分を消去',
'transfers.cancel': 'キャンセル',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 7ee35ab..5b72314 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -40,9 +40,18 @@ export default {
'register.loading': 'Account wordt aangemaakt...',
'register.has_account': 'Hebt u al een account?',
'register.login_link': 'Aanmelden',
- 'register.success_title': 'Account aangemaakt',
- 'register.success_msg': 'U kunt zich nu met uw gegevens aanmelden.',
+ 'register.success_title': 'Verifieer uw e-mail',
+ 'register.success_msg': 'Er is een verificatiecode naar uw e-mailadres gestuurd. Voer deze hieronder in om uw account te activeren.',
'register.go_login': 'Naar het aanmelden',
+ 'register.code_placeholder': 'Verificatiecode',
+ 'register.verify_btn': 'Verifiëren',
+ 'register.verifying': 'Verifiëren…',
+ 'register.verified_title': 'E-mail geverifieerd',
+ 'register.verified_msg': 'Uw account is actief. U kunt zich nu aanmelden.',
+ 'register.resend': 'Code opnieuw verzenden',
+ 'register.resend_sent': 'Er is een nieuwe code verzonden.',
+ 'login.pending_verification': 'Uw e-mailadres is nog niet geverifieerd. Controleer uw inbox voor de verificatiecode.',
+ 'login.verify_link': 'E-mail verifiëren',
'register.err_mismatch': 'De wachtwoorden komen niet overeen',
'register.err_min_len': {
one: 'Gebruik minstens {n} teken',
@@ -261,9 +270,14 @@ export default {
'settings.profile': 'Profiel',
'settings.username': 'Gebruikersnaam',
'settings.email': 'Email',
- 'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
- 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.email_save': 'Opslaan',
+ 'settings.email_saved': 'E-mail bijgewerkt',
+ 'settings.email_code_sent': 'Er is een verificatiecode naar uw nieuwe e-mailadres gestuurd.',
+ 'settings.email_code_placeholder': 'Verificatiecode',
+ 'settings.email_verify_btn': 'Verifiëren',
+ 'settings.email_verifying': 'Verifiëren…',
+ 'settings.email_verified': 'E-mailadres succesvol gewijzigd.',
+ 'settings.notif_global_disable': 'Alle meldingen uitschakelen',
'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
'settings.defaults': 'Standaardwaarden',
'settings.default_tab': 'Default tab',
@@ -468,10 +482,11 @@ export default {
+ 'via de hub.',
'members.pair_btn': 'Koppelen',
'members.pair_success': 'Deze browser is nu aan de node gekoppeld.',
- 'members.invite_code_ready': 'Uitnodigingscode voor {user} — stuur die op de manier '
- + 'waarop u elkaar gewoonlijk spreekt. Hij werkt één keer en gaat nooit via de hub.',
- 'members.invite_code_hint': 'De persoon voert hem in de eerste keer dat deze groep '
- + 'wordt geopend. U hoeft dan niet online te zijn.',
+ 'members.invite_code_ready': 'Uitnodigingscode voor {user}:',
+ 'members.invite_email_sent': 'Er is een e-mail met de code naar dit lid gestuurd.',
+ 'members.invite_email_failed': 'Kon de e-mail niet verzenden — deel de code handmatig.',
+ 'members.invite_code_hint': 'U kunt deze code ook via een ander kanaal delen (bijv. sms). '
+ + 'De code werkt één keer en gaat nooit via de hub.',
'transfers.title': 'Overdrachten',
'transfers.clear': 'Voltooide wissen',
'transfers.cancel': 'Annuleren',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index c221cea..bb2361e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -43,9 +43,18 @@ export default {
'register.loading': 'Tworzenie konta...',
'register.has_account': 'Konto już istnieje?',
'register.login_link': 'Zaloguj',
- 'register.success_title': 'Konto zostało założone',
- 'register.success_msg': 'Można się już zalogować przy użyciu swoich danych.',
+ 'register.success_title': 'Zweryfikuj swój e-mail',
+ 'register.success_msg': 'Kod weryfikacyjny został wysłany na Twój adres e-mail. Wpisz go poniżej, aby aktywować konto.',
'register.go_login': 'Przejdź do logowania',
+ 'register.code_placeholder': 'Kod weryfikacyjny',
+ 'register.verify_btn': 'Zweryfikuj',
+ 'register.verifying': 'Weryfikacja…',
+ 'register.verified_title': 'E-mail zweryfikowany',
+ 'register.verified_msg': 'Twoje konto jest aktywne. Możesz się teraz zalogować.',
+ 'register.resend': 'Wyślij kod ponownie',
+ 'register.resend_sent': 'Nowy kod został wysłany.',
+ 'login.pending_verification': 'Twój adres e-mail nie został jeszcze zweryfikowany. Sprawdź skrzynkę odbiorczą.',
+ 'login.verify_link': 'Zweryfikuj e-mail',
'register.err_mismatch': 'Hasła nie są zgodne',
'register.err_min_len': {
one: 'Proszę użyć co najmniej {n} znaku',
@@ -272,10 +281,15 @@ export default {
'settings.profile': 'Profil',
'settings.username': 'Nazwa użytkownika',
'settings.email': 'Email',
- 'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
- 'settings.notif_global_disable': 'Disable all notifications',
- 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.email_save': 'Zapisz',
+ 'settings.email_saved': 'E-mail zaktualizowany',
+ 'settings.email_code_sent': 'Kod weryfikacyjny został wysłany na nowy adres e-mail.',
+ 'settings.email_code_placeholder': 'Kod weryfikacyjny',
+ 'settings.email_verify_btn': 'Zweryfikuj',
+ 'settings.email_verifying': 'Weryfikacja…',
+ 'settings.email_verified': 'Adres e-mail został pomyślnie zmieniony.',
+ 'settings.notif_global_disable': 'Wyłącz wszystkie powiadomienia',
+ 'settings.notif_global_hint': 'Po włączeniu nie będą tworzone żadne powiadomienia dla żadnej grupy.',
'settings.defaults': 'Wartości domyślne',
'settings.default_tab': 'Default tab',
'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
@@ -480,11 +494,11 @@ export default {
+ 'przechodzi przez hub.',
'members.pair_btn': 'Sparuj',
'members.pair_success': 'Ta przeglądarka jest już sparowana z node.',
- 'members.invite_code_ready': 'Kod zaproszenia dla użytkownika {user} — proszę '
- + 'przekazać go tak, jak zwykle się Państwo kontaktują. Działa jednorazowo i nigdy '
- + 'nie przechodzi przez hub.',
- 'members.invite_code_hint': 'Osoba zaproszona wpisze go, otwierając tę grupę po raz '
- + 'pierwszy. Nie trzeba być wtedy dostępnym.',
+ 'members.invite_code_ready': 'Kod zaproszenia dla użytkownika {user}:',
+ 'members.invite_email_sent': 'E-mail z kodem został wysłany do tego członka.',
+ 'members.invite_email_failed': 'Nie udało się wysłać e-maila — przekaż kod ręcznie.',
+ 'members.invite_code_hint': 'Możesz też przekazać ten kod innym kanałem (np. SMS). '
+ + 'Kod działa jednorazowo i nigdy nie przechodzi przez hub.',
'transfers.title': 'Transfery',
'transfers.clear': 'Wyczyść zakończone',
'transfers.cancel': 'Anuluj',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 89c76c2..3fbbb0b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -41,9 +41,18 @@ export default {
'register.loading': 'Criando a conta...',
'register.has_account': 'Já tem uma conta?',
'register.login_link': 'Entrar',
- 'register.success_title': 'Conta criada',
- 'register.success_msg': 'Agora você pode entrar com suas credenciais.',
+ 'register.success_title': 'Verifique seu e-mail',
+ 'register.success_msg': 'Um código de verificação foi enviado para o seu e-mail. Digite-o abaixo para ativar sua conta.',
'register.go_login': 'Ir para o login',
+ 'register.code_placeholder': 'Código de verificação',
+ 'register.verify_btn': 'Verificar',
+ 'register.verifying': 'Verificando…',
+ 'register.verified_title': 'E-mail verificado',
+ 'register.verified_msg': 'Sua conta está ativa. Agora você pode entrar.',
+ 'register.resend': 'Reenviar código',
+ 'register.resend_sent': 'Um novo código foi enviado.',
+ 'login.pending_verification': 'Seu e-mail ainda não foi verificado. Verifique sua caixa de entrada.',
+ 'login.verify_link': 'Verificar e-mail',
'register.err_mismatch': 'As senhas não coincidem',
'register.err_min_len': {
one: 'Use pelo menos {n} caractere',
@@ -261,10 +270,15 @@ export default {
'settings.profile': 'Perfil',
'settings.username': 'Nome de usuário',
'settings.email': 'Email',
- 'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
- 'settings.notif_global_disable': 'Disable all notifications',
- 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.email_save': 'Salvar',
+ 'settings.email_saved': 'E-mail atualizado',
+ 'settings.email_code_sent': 'Um código de verificação foi enviado para o seu novo endereço de e-mail.',
+ 'settings.email_code_placeholder': 'Código de verificação',
+ 'settings.email_verify_btn': 'Verificar',
+ 'settings.email_verifying': 'Verificando…',
+ 'settings.email_verified': 'Endereço de e-mail alterado com sucesso.',
+ 'settings.notif_global_disable': 'Desativar todas as notificações',
+ 'settings.notif_global_hint': 'Quando ativado, nenhuma notificação é criada para nenhum grupo.',
'settings.defaults': 'Padrões',
'settings.default_tab': 'Default tab',
'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
@@ -466,10 +480,11 @@ export default {
+ 'passa pelo hub.',
'members.pair_btn': 'Parear',
'members.pair_success': 'Este navegador agora está pareado com o node.',
- 'members.invite_code_ready': 'Código de convite para {user} — envie a ele pelo meio '
- + 'que vocês costumam usar. Funciona uma única vez e nunca passa pelo hub.',
- 'members.invite_code_hint': 'A pessoa digita o código na primeira vez que abrir este '
- + 'grupo. Você não precisa estar on-line nesse momento.',
+ 'members.invite_code_ready': 'Código de convite para {user}:',
+ 'members.invite_email_sent': 'Um e-mail com o código foi enviado a este membro.',
+ 'members.invite_email_failed': 'Não foi possível enviar o e-mail — compartilhe o código manualmente.',
+ 'members.invite_code_hint': 'Você também pode compartilhar este código por outro canal (ex: SMS). '
+ + 'O código funciona uma única vez e nunca passa pelo hub.',
'transfers.title': 'Transferências',
'transfers.clear': 'Limpar as concluídas',
'transfers.cancel': 'Cancelar',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 105ad43..8dff38f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -40,9 +40,18 @@ export default {
'register.loading': '正在创建账户…',
'register.has_account': '已经有账户了?',
'register.login_link': '登录',
- 'register.success_title': '账户已创建',
- 'register.success_msg': '您现在可以使用您的凭据登录了。',
+ 'register.success_title': '验证您的邮箱',
+ 'register.success_msg': '验证码已发送至您的邮箱。请在下方输入以激活账户。',
'register.go_login': '前往登录',
+ 'register.code_placeholder': '验证码',
+ 'register.verify_btn': '验证',
+ 'register.verifying': '验证中…',
+ 'register.verified_title': '邮箱已验证',
+ 'register.verified_msg': '您的账户已激活。现在可以登录了。',
+ 'register.resend': '重新发送验证码',
+ 'register.resend_sent': '新验证码已发送。',
+ 'login.pending_verification': '您的邮箱尚未验证。请查看收件箱中的验证码。',
+ 'login.verify_link': '验证邮箱',
'register.err_mismatch': '两次输入的密码不一致',
'register.err_min_len': {
other: '请至少使用 {n} 个字符',
@@ -253,10 +262,15 @@ export default {
'settings.profile': '个人资料',
'settings.username': '用户名',
'settings.email': 'Email',
- 'settings.email_save': 'Save',
- 'settings.email_saved': 'Email updated',
- 'settings.notif_global_disable': 'Disable all notifications',
- 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.email_save': '保存',
+ 'settings.email_saved': '邮箱已更新',
+ 'settings.email_code_sent': '验证码已发送至您的新邮箱地址。',
+ 'settings.email_code_placeholder': '验证码',
+ 'settings.email_verify_btn': '验证',
+ 'settings.email_verifying': '验证中…',
+ 'settings.email_verified': '邮箱地址修改成功。',
+ 'settings.notif_global_disable': '关闭所有通知',
+ 'settings.notif_global_hint': '启用后,所有群组都不会创建通知。',
'settings.defaults': '默认值',
'settings.default_tab': 'Default tab',
'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
@@ -447,9 +461,11 @@ export default {
+ '该配对码绝不会经过 hub。',
'members.pair_btn': '配对',
'members.pair_success': '此浏览器现已与该 node 配对。',
- 'members.invite_code_ready': '给 {user} 的邀请码——用您平时联系的方式发给对方。'
- + '它只能使用一次,并且绝不会经过 hub。',
- 'members.invite_code_hint': '对方第一次打开此群组时输入即可。那时您不需要在线。',
+ 'members.invite_code_ready': '给 {user} 的邀请码:',
+ 'members.invite_email_sent': '包含验证码的邮件已发送给该成员。',
+ 'members.invite_email_failed': '无法发送邮件——请手动分享验证码。',
+ 'members.invite_code_hint': '您也可以通过其他渠道(如短信)分享此验证码。'
+ + '验证码仅可使用一次,且绝不会经过 hub。',
'transfers.title': '传输',
'transfers.clear': '清除已完成',
'transfers.cancel': '取消',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
index 3d3e578..ce22116 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
@@ -15,6 +15,9 @@ export function ProfilePage({ user, onLogout }) {
const [emailEditing, setEmailEditing] = useState(false);
const [emailSaving, setEmailSaving] = useState(false);
const [emailStatus, setEmailStatus] = useState('');
+ const [emailVerifyPending, setEmailVerifyPending] = useState(false);
+ const [emailCode, setEmailCode] = useState('');
+ const [emailVerifying, setEmailVerifying] = useState(false);
const [pinCount, setPinCount] = useState(
() => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0));
const [delOpen, setDelOpen] = useState(false);
@@ -63,13 +66,18 @@ export function ProfilePage({ user, onLogout }) {
setEmailSaving(true);
setEmailStatus('');
try {
- await hubFetch('/v1/users/me', {
+ const resp = await hubFetch('/v1/users/me', {
method: 'PATCH', token: user.token, body: { email: val },
});
- setEmail(val);
- setEmailEditing(false);
- setEmailStatus(t('settings.email_saved'));
- setTimeout(() => setEmailStatus(''), 3000);
+ if (resp.email_verification_required) {
+ setEmailVerifyPending(true);
+ setEmailStatus(t('settings.email_code_sent'));
+ } else {
+ setEmail(val);
+ setEmailEditing(false);
+ setEmailStatus(t('settings.email_saved'));
+ setTimeout(() => setEmailStatus(''), 3000);
+ }
} catch (e) {
setEmailStatus(e.message);
} finally {
@@ -77,6 +85,27 @@ export function ProfilePage({ user, onLogout }) {
}
}, [emailDraft, email, user.token]);
+ const verifyEmailChange = useCallback(async () => {
+ if (!emailCode.trim()) return;
+ setEmailVerifying(true);
+ setEmailStatus('');
+ try {
+ const resp = await hubFetch('/v1/users/verify-email-change', {
+ method: 'POST', token: user.token, body: { code: emailCode.trim() },
+ });
+ setEmail(resp.email || emailDraft.trim());
+ setEmailEditing(false);
+ setEmailVerifyPending(false);
+ setEmailCode('');
+ setEmailStatus(t('settings.email_verified'));
+ setTimeout(() => setEmailStatus(''), 3000);
+ } catch (e) {
+ setEmailStatus(e.message);
+ } finally {
+ setEmailVerifying(false);
+ }
+ }, [emailCode, emailDraft, user.token]);
+
const submitNodeKey = useCallback(async () => {
const key = nodeKey.trim();
if (!key) return;
@@ -110,16 +139,19 @@ export function ProfilePage({ user, onLogout }) {
<div class="settings-row">
<span class="settings-label">${t('settings.email')}</span>
${emailEditing
- ? html`<span style="display:flex;gap:8px;align-items:center">
+ ? html`<span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="email" value=${emailDraft}
onInput=${e => setEmailDraft(e.target.value)}
onKeyDown=${e => e.key === 'Enter' && saveEmail()}
+ disabled=${emailVerifyPending}
style="font-size:0.9em;padding:4px 8px;border:1px solid var(--border);border-radius:4px" />
- <button class="admin-btn" onClick=${saveEmail}
- disabled=${emailSaving}>${t('settings.email_save')}</button>
- <button class="btn-secondary" onClick=${() => {
- setEmailEditing(false); setEmailDraft(email);
- }}>${t('settings.cancel')}</button>
+ ${!emailVerifyPending && html`
+ <button class="admin-btn" onClick=${saveEmail}
+ disabled=${emailSaving}>${t('settings.email_save')}</button>
+ <button class="btn-secondary" onClick=${() => {
+ setEmailEditing(false); setEmailDraft(email);
+ }}>${t('settings.cancel')}</button>
+ `}
</span>`
: html`<span style="display:flex;gap:8px;align-items:center">
<span class="settings-value">${email || '—'}</span>
@@ -128,6 +160,23 @@ export function ProfilePage({ user, onLogout }) {
</span>`
}
</div>
+ ${emailVerifyPending && html`
+ <div style="display:flex;gap:8px;align-items:center;margin-top:8px">
+ <input type="text" placeholder="${t('settings.email_code_placeholder')}"
+ value=${emailCode} onInput=${e => setEmailCode(e.target.value)}
+ onKeyDown=${e => e.key === 'Enter' && verifyEmailChange()}
+ inputmode="numeric" maxlength="6"
+ style="width:120px;text-align:center;font-size:1.1em;letter-spacing:0.2em" />
+ <button class="admin-btn" onClick=${verifyEmailChange}
+ disabled=${emailVerifying}>
+ ${emailVerifying ? t('settings.email_verifying') : t('settings.email_verify_btn')}
+ </button>
+ <button class="btn-secondary" onClick=${() => {
+ setEmailVerifyPending(false); setEmailEditing(false);
+ setEmailDraft(email); setEmailCode(''); setEmailStatus('');
+ }}>${t('settings.cancel')}</button>
+ </div>
+ `}
${emailStatus && html`<p class="settings-hint" style="margin-top:4px">${emailStatus}</p>`}
</div>