diff --git a/server/errors.go b/server/errors.go index 51bfbe21..d204aa53 100644 --- a/server/errors.go +++ b/server/errors.go @@ -157,7 +157,7 @@ var ( errHTTPConflictProvisionedUserChange = &errHTTP{40905, http.StatusConflict, "conflict: cannot change or delete provisioned user", "", nil} errHTTPConflictProvisionedTokenChange = &errHTTP{40906, http.StatusConflict, "conflict: cannot change or delete provisioned token", "", nil} errHTTPConflictEmailExists = &errHTTP{40907, http.StatusConflict, "conflict: email address already exists", "", nil} - errHTTPConflictEmailPrimaryElsewhere = &errHTTP{40908, http.StatusConflict, "conflict: email address is the recovery email on another account", "", nil} + errHTTPConflictEmailPrimaryElsewhere = &errHTTP{40908, http.StatusConflict, "conflict: email address is the primary email on another account", "", nil} errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil} errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil} diff --git a/user/types.go b/user/types.go index 44aecd5d..ce7efe36 100644 --- a/user/types.go +++ b/user/types.go @@ -296,7 +296,7 @@ var ( ErrPhoneNumberExists = errors.New("phone number already exists") ErrEmailNotFound = errors.New("email not found") ErrEmailExists = errors.New("email already exists") - ErrEmailPrimaryElsewhere = errors.New("email is the recovery email on another account") + ErrEmailPrimaryElsewhere = errors.New("email is the primary email on another account") ErrMagicLinkNotFound = errors.New("magic link not found") ErrProvisionedUserChange = errors.New("cannot change or delete provisioned user") ErrProvisionedTokenChange = errors.New("cannot change or delete provisioned token") diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index cc78f3d4..278d6466 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -29,11 +29,11 @@ "login_link_signup": "Sign up", "login_link_forgot_password": "Forgot password?", "reset_password_request_title": "Reset password", - "reset_password_request_description": "Enter your username or email address. If an account exists, we'll email a link to reset your password.", + "reset_password_request_description": "Enter your username or email address. If an account exists, a link to reset your password will be emailed.", "reset_password_request_identifier_label": "Username or email", "reset_password_request_button_submit": "Send reset link", "reset_password_sent_title": "Check your inbox", - "reset_password_sent_description": "If an account exists, we've emailed a link to reset your password.", + "reset_password_sent_description": "If an account exists, a link to reset your password has been emailed.", "reset_password_back_to_login": "Back to sign-in", "reset_password_disabled": "Password reset is not enabled on this server.", "reset_password_title": "Set a new password", @@ -247,16 +247,17 @@ "account_basics_emails_primary_badge": "Primary", "account_basics_emails_chip_actions": "Click for actions", "account_basics_emails_unverified": "unverified", - "account_basics_emails_set_primary": "Set as recovery email", + "account_basics_emails_set_primary": "Set as primary email", "account_basics_emails_delete": "Remove", "account_basics_emails_cancel": "Cancel", "account_basics_emails_resend": "Resend verification email", "account_basics_emails_resent": "Verification email sent, check your inbox", - "account_basics_emails_primary_elsewhere": "This email is the recovery email on another account", - "account_basics_emails_no_recovery_warning": "Add a recovery email address to ensure you can reset your password.", - "account_basics_emails_provisioned_info": "Provisioned users cannot add a recovery email address.", + "account_basics_emails_primary_elsewhere": "This email is the primary email on another account", + "account_basics_emails_no_recovery_warning": "Add at least one email address to ensure you can recover your account if you lose your password.", + "account_basics_emails_no_primary_warning": "Add a primary email address to ensure you can recover your account if you lose your password.", + "account_basics_emails_provisioned_info": "Provisioned users cannot add a primary email address, but you can still add an email address for notifications.", "account_basics_emails_dialog_title": "Add email address", - "account_basics_emails_dialog_description": "Enter an email address to add it to your account. We will send a verification link to confirm it is yours.", + "account_basics_emails_dialog_description": "Enter an email address to add it to your account. A verification link will be sent to confirm it is yours.", "account_basics_emails_dialog_email_label": "Email address", "account_basics_emails_dialog_email_placeholder": "e.g. user@example.com", "account_basics_emails_dialog_verify_button": "Send verification link", @@ -269,8 +270,8 @@ "account_basics_tier_title": "Account type", "account_basics_tier_description": "Your account's power level", "account_basics_tier_admin": "Admin", - "account_basics_tier_admin_suffix_with_tier": "(with {{tier}} tier)", - "account_basics_tier_admin_suffix_no_tier": "(no tier)", + "account_basics_tier_admin_suffix_with_tier": "with {{tier}} tier", + "account_basics_tier_admin_suffix_no_tier": "no tier", "account_basics_tier_basic": "Basic", "account_basics_tier_provisioned": "Provisioned", "account_basics_tier_free": "Free", diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index e9e2168e..0024c70f 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -269,22 +269,23 @@ const AccountType = () => { } }; + // The account type is a base label ("Admin", "Basic", "Free", or the tier name) plus an optional + // qualifier chip (admin tier status, or the billing interval). let accountType; + let qualifierChip; if (account.role === Role.ADMIN) { - const tierSuffix = account.tier - ? t("account_basics_tier_admin_suffix_with_tier", { - tier: account.tier.name, - }) + accountType = t("account_basics_tier_admin"); + qualifierChip = account.tier + ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier"); - accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; } else if (!account.tier) { accountType = config.enable_payments ? t("account_basics_tier_free") : t("account_basics_tier_basic"); } else { accountType = account.tier.name; if (account.billing?.interval === SubscriptionInterval.MONTH) { - accountType += ` (${t("account_basics_tier_interval_monthly")})`; + qualifierChip = t("account_basics_tier_interval_monthly"); } else if (account.billing?.interval === SubscriptionInterval.YEAR) { - accountType += ` (${t("account_basics_tier_interval_yearly")})`; + qualifierChip = t("account_basics_tier_interval_yearly"); } } @@ -296,6 +297,7 @@ const AccountType = () => { >
{accountType} + {qualifierChip && } {account.provisioned && } {account.billing?.paid_until && !account.billing?.cancel_at && ( { const verifiedEmails = emails.filter((e) => !e.pending); const pendingEmails = emails.filter((e) => e.pending); const primaryEmail = verifiedEmails.find((e) => e.primary)?.address ?? ""; - // Provisioned users get their password from the server config and cannot reset it, so they don't - // get the "no recovery email" nudge (the Add-email dialog explains the recovery-email limitation). - const showNoRecoveryWarning = config.enable_reset_password && primaryEmail === "" && !account?.provisioned; + // Recovery nudges (skipped for provisioned users -- they can't reset, and the Add-email dialog + // explains the limitation): prompt for a first email when there are none, or for a primary when + // there are emails but none is primary. + const recoveryRelevant = config.enable_reset_password && !account?.provisioned; + const hasNoEmails = verifiedEmails.length === 0 && pendingEmails.length === 0; + const showNoEmailWarning = recoveryRelevant && hasNoEmails; + const showNoPrimaryWarning = recoveryRelevant && !hasNoEmails && primaryEmail === ""; return ( @@ -468,7 +474,7 @@ const Emails = () => { variant="outlined" onClick={(ev) => openMenu(ev, email)} onDelete={() => handleDelete(email.address)} - sx={email.primary ? { "& .MuiChip-icon": { color: "primary.main" } } : undefined} + sx={email.primary ? { "& .MuiChip-icon": { color: "#fbc02d" } } : undefined} /> ))} {pendingEmails.map((email) => ( @@ -491,11 +497,16 @@ const Emails = () => { - {showNoRecoveryWarning && ( + {showNoEmailWarning && ( {t("account_basics_emails_no_recovery_warning")} )} + {showNoPrimaryWarning && ( + + {t("account_basics_emails_no_primary_warning")} + + )}
runMenuAction(handleCopy)}> diff --git a/web/src/components/EmailVerify.jsx b/web/src/components/EmailVerify.jsx index 8013652e..d1f09783 100644 --- a/web/src/components/EmailVerify.jsx +++ b/web/src/components/EmailVerify.jsx @@ -49,7 +49,7 @@ const EmailVerify = () => { {status === "success" && ( <> - + {t("email_verify_success_title")} {t("email_verify_success_description")} @@ -61,7 +61,7 @@ const EmailVerify = () => { {status === "error" && ( <> - + {t("email_verify_error_title")} {t("email_verify_error_description")} diff --git a/web/src/components/PasswordReset.jsx b/web/src/components/PasswordReset.jsx index c2a2613e..091a22c6 100644 --- a/web/src/components/PasswordReset.jsx +++ b/web/src/components/PasswordReset.jsx @@ -51,7 +51,7 @@ const PasswordReset = () => { return ( - + {t("reset_password_success_title")} {t("reset_password_success_description")} diff --git a/web/src/components/ResetPassword.jsx b/web/src/components/ResetPassword.jsx index 05d95e21..6b765d5f 100644 --- a/web/src/components/ResetPassword.jsx +++ b/web/src/components/ResetPassword.jsx @@ -48,7 +48,7 @@ const ResetPassword = () => { return ( - + {t("reset_password_sent_title")} {t("reset_password_sent_description")}