mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(settings): group account security into clear cards (#3359)
This commit is contained in:
1 parent
c8755885d4
commit
72f9f9c396
43 files changed
+9982
-9367
No files matched your search
+134
@@ -0,0 +1,134 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border: 0.0625rem solid var(--background-modifier-accent);
|
||||
border-radius: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.headerless .rows > :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.headerText {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.headerAction {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
border-top: 0.0625rem solid var(--background-modifier-accent);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.rowText {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rowLabel {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.rowDescription {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.rowWarning {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: var(--alert-warning-color);
|
||||
}
|
||||
|
||||
.rowActions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.emptyRow {
|
||||
border-top: 0.0625rem solid var(--background-modifier-accent);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.switchRow {
|
||||
border-top: 0.0625rem solid var(--background-modifier-accent);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import statusStyles from '@app/features/app/components/dialogs/components/plutonium/PurchaseHistoryStatus.module.css';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/AccountSecurityCard.module.css';
|
||||
import {clsx} from 'clsx';
|
||||
import type React from 'react';
|
||||
import {useId} from 'react';
|
||||
|
||||
type AccountSecurityStatusTone = 'success' | 'muted' | 'neutral' | 'pending';
|
||||
|
||||
interface AccountSecurityCardProps {
|
||||
title?: React.ReactNode;
|
||||
'aria-label'?: string;
|
||||
description?: React.ReactNode;
|
||||
status?: {label: React.ReactNode; tone: AccountSecurityStatusTone};
|
||||
action?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
'data-flx'?: string;
|
||||
}
|
||||
|
||||
export const AccountSecurityCard: React.FC<AccountSecurityCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
action,
|
||||
children,
|
||||
'aria-label': ariaLabel,
|
||||
}) => {
|
||||
const titleId = useId();
|
||||
const hasHeader = title != null || action != null;
|
||||
return (
|
||||
<section
|
||||
className={clsx(styles.card, !hasHeader && styles.headerless)}
|
||||
aria-labelledby={title != null ? titleId : undefined}
|
||||
aria-label={title != null ? undefined : ariaLabel}
|
||||
data-flx="user.account-security-card.card"
|
||||
>
|
||||
{hasHeader ? (
|
||||
<div className={styles.header} data-flx="user.account-security-card.header">
|
||||
<div className={styles.headerText} data-flx="user.account-security-card.header-text">
|
||||
<div className={styles.titleRow} data-flx="user.account-security-card.title-row">
|
||||
{title != null ? (
|
||||
<h4 id={titleId} className={styles.title} data-flx="user.account-security-card.title">
|
||||
{title}
|
||||
</h4>
|
||||
) : null}
|
||||
{status ? (
|
||||
<span className={statusStyles[status.tone]} data-flx="user.account-security-card.status">
|
||||
{status.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{description ? (
|
||||
<p className={styles.description} data-flx="user.account-security-card.description">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? (
|
||||
<div className={styles.headerAction} data-flx="user.account-security-card.header-action">
|
||||
{action}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{children ? (
|
||||
<div className={styles.rows} data-flx="user.account-security-card.rows">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
interface AccountSecurityRowProps {
|
||||
label: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
warning?: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
children?: React.ReactNode;
|
||||
'data-flx'?: string;
|
||||
}
|
||||
|
||||
export const AccountSecurityRow: React.FC<AccountSecurityRowProps> = ({
|
||||
label,
|
||||
description,
|
||||
warning,
|
||||
labelClassName,
|
||||
children,
|
||||
}) => (
|
||||
<div className={styles.row} data-flx="user.account-security-card.row">
|
||||
<div className={styles.rowText} data-flx="user.account-security-card.row-text">
|
||||
<span className={labelClassName ?? styles.rowLabel} data-flx="user.account-security-card.row-label">
|
||||
{label}
|
||||
</span>
|
||||
{description ? (
|
||||
<span className={styles.rowDescription} data-flx="user.account-security-card.row-description">
|
||||
{description}
|
||||
</span>
|
||||
) : null}
|
||||
{warning ? (
|
||||
<span className={styles.rowWarning} data-flx="user.account-security-card.row-warning">
|
||||
{warning}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{children ? (
|
||||
<div className={styles.rowActions} data-flx="user.account-security-card.row-actions">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const AccountSecurityEmptyRow: React.FC<{children: React.ReactNode; 'data-flx'?: string}> = ({children}) => (
|
||||
<div className={styles.emptyRow} data-flx="user.account-security-card.empty-row">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const AccountSecuritySwitchRow: React.FC<{children: React.ReactNode; 'data-flx'?: string}> = ({children}) => (
|
||||
<div className={styles.switchRow} data-flx="user.account-security-card.switch-row">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
+5
-72
@@ -1,60 +1,15 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.accountRows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.emailRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.emailRow {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.emailText {
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.emailTextSelectable {
|
||||
@@ -63,33 +18,11 @@
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
margin-top: 0.1em;
|
||||
text-align: left;
|
||||
color: var(--text-link);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggleButton:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.toggleButton {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.warningText {
|
||||
color: var(--alert-warning-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 0.0625rem solid var(--background-header-secondary);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
align-self: flex-start;
|
||||
}
|
||||
+105
-150
@@ -9,6 +9,10 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {EmailChangeModal} from '@app/features/user/components/modals/EmailChangeModal';
|
||||
import {PasswordChangeModal} from '@app/features/user/components/modals/PasswordChangeModal';
|
||||
import {
|
||||
AccountSecurityCard,
|
||||
AccountSecurityRow,
|
||||
} from '@app/features/user/components/modals/tabs/account_security_tab/AccountSecurityCard';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/AccountTab.module.css';
|
||||
import {
|
||||
RecoveryKitReminderAlert,
|
||||
@@ -29,6 +33,10 @@ const REVEAL_EMAIL_TOGGLE_DESCRIPTOR = msg({
|
||||
message: 'Reveal',
|
||||
comment: 'Account settings: button that reveals the masked user email address. Verb, sentence case.',
|
||||
});
|
||||
const SIGN_IN_METHODS_DESCRIPTOR = msg({
|
||||
message: 'Sign-in methods',
|
||||
comment: 'Accessible name for the group of email, password and recovery kit rows in account settings.',
|
||||
});
|
||||
const maskEmail = (email: string): string => {
|
||||
const [username, domain] = email.split('@');
|
||||
const maskedUsername = username.replace(/./g, '*');
|
||||
@@ -47,166 +55,107 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
|
||||
const {i18n} = useLingui();
|
||||
const usesUsernameSignIn = RuntimeConfig.usesUsernameSignIn;
|
||||
const emailRow = usesUsernameSignIn ? null : isClaimed ? (
|
||||
<>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.account-tab.account-tab-content.email-row">
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-row-content"
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Email address</Trans>}
|
||||
description={
|
||||
<span
|
||||
className={styles.emailRow}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-value"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-label"
|
||||
<span
|
||||
className={`${styles.emailText} ${showMaskedEmail ? styles.emailTextSelectable : ''}`}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-text"
|
||||
>
|
||||
<Trans>Email address</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.emailRow}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-value-row"
|
||||
{showMaskedEmail ? user.email : maskEmail(user.email!)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggleButton}
|
||||
onClick={() => setShowMaskedEmail(!showMaskedEmail)}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.toggle-button"
|
||||
>
|
||||
<span
|
||||
className={`${styles.emailText} ${showMaskedEmail ? styles.emailTextSelectable : ''}`}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-text"
|
||||
>
|
||||
{showMaskedEmail ? user.email : maskEmail(user.email!)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggleButton}
|
||||
onClick={() => setShowMaskedEmail(!showMaskedEmail)}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.toggle-email-visibility"
|
||||
>
|
||||
{showMaskedEmail ? i18n._(HIDE_EMAIL_TOGGLE_DESCRIPTOR) : i18n._(REVEAL_EMAIL_TOGGLE_DESCRIPTOR)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{RuntimeConfig.emailsEnabled && (
|
||||
<Button
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<EmailChangeModal
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-change-modal"
|
||||
/>
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.change-email-button"
|
||||
>
|
||||
<Trans>Change email</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{RuntimeConfig.emailsEnabled && user.email && !user.verified && (
|
||||
<EmailVerificationAlert data-flx="user.account-security-tab.account-tab.account-tab-content.email-verification-alert" />
|
||||
{showMaskedEmail ? i18n._(HIDE_EMAIL_TOGGLE_DESCRIPTOR) : i18n._(REVEAL_EMAIL_TOGGLE_DESCRIPTOR)}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-row"
|
||||
>
|
||||
{RuntimeConfig.emailsEnabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<EmailChangeModal
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-change-modal"
|
||||
/>
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.button.change-email"
|
||||
>
|
||||
<Trans>Change email</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</AccountSecurityRow>
|
||||
) : (
|
||||
<div className={styles.row} data-flx="user.account-security-tab.account-tab.account-tab-content.email-row">
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-label"
|
||||
>
|
||||
<Trans>Email address</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.warningText}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-warning"
|
||||
>
|
||||
<Trans>No email address set</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Email address</Trans>}
|
||||
warning={<Trans>No email address set</Trans>}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.email-row--unclaimed"
|
||||
>
|
||||
<Button
|
||||
small={true}
|
||||
className={styles.claimButton}
|
||||
fitContent
|
||||
onClick={() => openClaimAccountModal()}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.add-email-button"
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.button.add-email"
|
||||
>
|
||||
<Trans>Add email</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</AccountSecurityRow>
|
||||
);
|
||||
const passwordRow = (
|
||||
<div
|
||||
className={emailRow ? styles.divider : undefined}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-block"
|
||||
>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.account-tab.account-tab-content.password-row">
|
||||
{isClaimed ? (
|
||||
<>
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-label"
|
||||
>
|
||||
<Trans>Password</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.description}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-description"
|
||||
>
|
||||
{user.passwordLastChangedAt ? (
|
||||
<Trans>Last changed: {DateUtils.getRelativeDateString(user.passwordLastChangedAt, i18n)}</Trans>
|
||||
) : (
|
||||
<Trans>Last changed: never</Trans>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<PasswordChangeModal data-flx="user.account-security-tab.account-tab.account-tab-content.password-change-modal" />
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.change-password-button"
|
||||
>
|
||||
<Trans>Change password</Trans>
|
||||
</Button>
|
||||
</>
|
||||
const passwordRow = isClaimed ? (
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Password</Trans>}
|
||||
description={
|
||||
user.passwordLastChangedAt ? (
|
||||
<Trans>Last changed: {DateUtils.getRelativeDateString(user.passwordLastChangedAt, i18n)}</Trans>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-label"
|
||||
>
|
||||
<Trans>Password</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.warningText}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-warning"
|
||||
>
|
||||
<Trans>No password set</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
small={true}
|
||||
className={styles.claimButton}
|
||||
fitContent
|
||||
onClick={() => openClaimAccountModal()}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.set-password-button"
|
||||
>
|
||||
<Trans>Set password</Trans>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Trans>Last changed: never</Trans>
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-row"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<PasswordChangeModal data-flx="user.account-security-tab.account-tab.account-tab-content.password-change-modal" />
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.button.change-password"
|
||||
>
|
||||
<Trans>Change password</Trans>
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
) : (
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Password</Trans>}
|
||||
warning={<Trans>No password set</Trans>}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.password-row--unclaimed"
|
||||
>
|
||||
<Button
|
||||
small={true}
|
||||
onClick={() => openClaimAccountModal()}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.button.set-password"
|
||||
>
|
||||
<Trans>Set password</Trans>
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -216,10 +165,13 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
|
||||
{usesUsernameSignIn && isClaimed && (
|
||||
<RecoveryKitReminderAlert
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.recovery-kit-reminder"
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.recovery-kit-reminder-alert"
|
||||
/>
|
||||
)}
|
||||
<div className={styles.accountRows} data-flx="user.account-security-tab.account-tab.account-tab-content.rows">
|
||||
<AccountSecurityCard
|
||||
aria-label={i18n._(SIGN_IN_METHODS_DESCRIPTOR)}
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.rows"
|
||||
>
|
||||
{emailRow}
|
||||
{passwordRow}
|
||||
{usesUsernameSignIn && isClaimed && (
|
||||
@@ -228,7 +180,10 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
|
||||
data-flx="user.account-security-tab.account-tab.account-tab-content.recovery-kit-row"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AccountSecurityCard>
|
||||
{RuntimeConfig.emailsEnabled && isClaimed && !usesUsernameSignIn && user.email && !user.verified && (
|
||||
<EmailVerificationAlert data-flx="user.account-security-tab.account-tab.account-tab-content.email-verification-alert" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
+26
-17
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SettingsTabSection} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
|
||||
import {AccountDeleteModal} from '@app/features/auth/components/modals/AccountDeleteModal';
|
||||
import {AccountDisableModal} from '@app/features/auth/components/modals/AccountDisableModal';
|
||||
import {GuildOwnershipWarningModal} from '@app/features/guild/components/modals/GuildOwnershipWarningModal';
|
||||
@@ -9,12 +8,21 @@ import {DELETE_ACCOUNT_DESCRIPTOR, DISABLE_ACCOUNT_DESCRIPTOR} from '@app/featur
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/AccountTab.module.css';
|
||||
import {
|
||||
AccountSecurityCard,
|
||||
AccountSecurityRow,
|
||||
} from '@app/features/user/components/modals/tabs/account_security_tab/AccountSecurityCard';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
|
||||
const DANGER_ZONE_ACTIONS_DESCRIPTOR = msg({
|
||||
message: 'Disable or delete your account',
|
||||
comment: 'Accessible name for the group of disable and delete account actions in account settings.',
|
||||
});
|
||||
|
||||
interface DangerZoneTabProps {
|
||||
user: User;
|
||||
isClaimed: boolean;
|
||||
@@ -49,41 +57,42 @@ export const DangerZoneTabContent: React.FC<DangerZoneTabProps> = observer(({use
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<AccountSecurityCard
|
||||
aria-label={i18n._(DANGER_ZONE_ACTIONS_DESCRIPTOR)}
|
||||
data-flx="user.account-security-tab.danger-zone-tab.card"
|
||||
>
|
||||
{isClaimed && (
|
||||
<SettingsTabSection
|
||||
title={i18n._(DISABLE_ACCOUNT_DESCRIPTOR)}
|
||||
<AccountSecurityRow
|
||||
label={i18n._(DISABLE_ACCOUNT_DESCRIPTOR)}
|
||||
description={<Trans>Temporarily disable your account. You can reactivate it later by signing back in.</Trans>}
|
||||
data-flx="user.account-security-tab.danger-zone-tab.danger-zone-tab-content.settings-tab-section"
|
||||
data-flx="user.account-security-tab.danger-zone-tab.disable-row"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={styles.claimButton}
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={handleDisableAccount}
|
||||
data-flx="user.account-security-tab.danger-zone-tab.danger-zone-tab-content.claim-button.disable-account"
|
||||
data-flx="user.account-security-tab.danger-zone-tab.button.disable-account"
|
||||
>
|
||||
{i18n._(DISABLE_ACCOUNT_DESCRIPTOR)}
|
||||
</Button>
|
||||
</SettingsTabSection>
|
||||
</AccountSecurityRow>
|
||||
)}
|
||||
<SettingsTabSection
|
||||
title={i18n._(DELETE_ACCOUNT_DESCRIPTOR)}
|
||||
<AccountSecurityRow
|
||||
label={i18n._(DELETE_ACCOUNT_DESCRIPTOR)}
|
||||
description={
|
||||
<Trans>Permanently delete your account and all associated data. This action cannot be undone.</Trans>
|
||||
}
|
||||
data-flx="user.account-security-tab.danger-zone-tab.danger-zone-tab-content.settings-tab-section--2"
|
||||
data-flx="user.account-security-tab.danger-zone-tab.delete-row"
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
className={styles.claimButton}
|
||||
small={true}
|
||||
onClick={handleDeleteAccount}
|
||||
data-flx="user.account-security-tab.danger-zone-tab.danger-zone-tab-content.claim-button.delete-account"
|
||||
data-flx="user.account-security-tab.danger-zone-tab.button.delete-account"
|
||||
>
|
||||
{i18n._(DELETE_ACCOUNT_DESCRIPTOR)}
|
||||
</Button>
|
||||
</SettingsTabSection>
|
||||
</>
|
||||
</AccountSecurityRow>
|
||||
</AccountSecurityCard>
|
||||
);
|
||||
});
|
||||
+24
-31
@@ -6,7 +6,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {WarningAlert} from '@app/features/ui/warning_alert/WarningAlert';
|
||||
import * as RecoveryKitCommands from '@app/features/user/commands/RecoveryKitCommands';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/AccountTab.module.css';
|
||||
import {AccountSecurityRow} from '@app/features/user/components/modals/tabs/account_security_tab/AccountSecurityCard';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import RecoveryKitStatus from '@app/features/user/state/RecoveryKitStatus';
|
||||
import * as DateUtils from '@app/features/user/utils/DateFormatting';
|
||||
@@ -79,37 +79,30 @@ export const RecoveryKitRow = observer(({user}: {user: User}) => {
|
||||
const status = useRecoveryKitStatus(user);
|
||||
const {creating, create} = useCreateRecoveryKit();
|
||||
const createdAt = status?.createdAt ?? null;
|
||||
const missing = status != null && !status.hasRecoveryKit;
|
||||
return (
|
||||
<div className={styles.divider} data-flx="user.account-security-tab.recovery-kit-settings.recovery-kit-block">
|
||||
<div className={styles.row} data-flx="user.account-security-tab.recovery-kit-settings.recovery-kit-row">
|
||||
<div className={styles.rowContent} data-flx="user.account-security-tab.recovery-kit-settings.row-content">
|
||||
<div className={styles.label} data-flx="user.account-security-tab.recovery-kit-settings.label">
|
||||
<Trans>Recovery kit</Trans>
|
||||
</div>
|
||||
{status && !status.hasRecoveryKit ? (
|
||||
<div className={styles.warningText} data-flx="user.account-security-tab.recovery-kit-settings.missing">
|
||||
<Trans>You don't have a recovery kit yet</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.description} data-flx="user.account-security-tab.recovery-kit-settings.description">
|
||||
{createdAt ? (
|
||||
<Trans>Created {DateUtils.getRelativeDateString(createdAt, i18n)}</Trans>
|
||||
) : (
|
||||
<Trans>Lets you reset your password if you forget it.</Trans>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
small={true}
|
||||
submitting={creating}
|
||||
onClick={status?.hasRecoveryKit === false ? create : () => confirmNewRecoveryKit(i18n, create)}
|
||||
data-flx="user.account-security-tab.recovery-kit-settings.button.create"
|
||||
>
|
||||
{status?.hasRecoveryKit === false ? <Trans>Create recovery kit</Trans> : <Trans>Create a new kit</Trans>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Recovery kit</Trans>}
|
||||
description={
|
||||
missing ? null : createdAt ? (
|
||||
<Trans>Created {DateUtils.getRelativeDateString(createdAt, i18n)}</Trans>
|
||||
) : (
|
||||
<Trans>Lets you reset your password if you forget it.</Trans>
|
||||
)
|
||||
}
|
||||
warning={missing ? <Trans>You don't have a recovery kit yet</Trans> : null}
|
||||
data-flx="user.account-security-tab.recovery-kit-settings.recovery-kit-row"
|
||||
>
|
||||
<Button
|
||||
small={true}
|
||||
variant={missing ? 'primary' : 'secondary'}
|
||||
submitting={creating}
|
||||
onClick={status?.hasRecoveryKit === false ? create : () => confirmNewRecoveryKit(i18n, create)}
|
||||
data-flx="user.account-security-tab.recovery-kit-settings.button.create"
|
||||
>
|
||||
{status?.hasRecoveryKit === false ? <Trans>Create recovery kit</Trans> : <Trans>Create a new kit</Trans>}
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
-80
@@ -1,91 +1,11 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.warningText {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--alert-warning-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 0.0625rem solid var(--background-modifier-accent);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.passkeyList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.passkeyItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.passkeyItem {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.passkeyInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.passkeyName {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.passkeyDetails {
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.passkeyActions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
align-self: flex-start;
|
||||
}
|
||||
+195
-236
@@ -19,6 +19,12 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import * as UserCommands from '@app/features/user/commands/UserCommands';
|
||||
import {
|
||||
AccountSecurityCard,
|
||||
AccountSecurityEmptyRow,
|
||||
AccountSecurityRow,
|
||||
AccountSecuritySwitchRow,
|
||||
} from '@app/features/user/components/modals/tabs/account_security_tab/AccountSecurityCard';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/SecurityTab.module.css';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import type {WebAuthnCredential} from '@app/features/user/state/WebAuthnCredentials';
|
||||
@@ -85,9 +91,54 @@ const COULD_NOT_UPDATE_PASSKEY_TWO_FACTOR_DESCRIPTOR = msg({
|
||||
message: "Couldn't update passkey two-factor authentication",
|
||||
comment: 'Title of the error modal shown when the passkey two-factor setting could not be saved.',
|
||||
});
|
||||
const ACCOUNT_ACCESS_DESCRIPTOR = msg({
|
||||
message: 'Account access',
|
||||
comment: 'Security settings section for third-party app access and signed-in devices.',
|
||||
const APPS_AND_DEVICES_DESCRIPTOR = msg({
|
||||
message: 'Apps and devices',
|
||||
comment: 'Security settings card title for third-party app access and signed-in devices.',
|
||||
});
|
||||
const TWO_FACTOR_CARD_DESCRIPTION_DESCRIPTOR = msg({
|
||||
message: 'Ask for a second step after your password when you sign in.',
|
||||
comment: 'Security settings: description under the two-factor authentication card title.',
|
||||
});
|
||||
const STATUS_ON_DESCRIPTOR = msg({
|
||||
message: 'On',
|
||||
comment:
|
||||
'Security settings: status badge next to the two-factor authentication title when it is turned on. One word.',
|
||||
});
|
||||
const STATUS_OFF_DESCRIPTOR = msg({
|
||||
message: 'Off',
|
||||
comment:
|
||||
'Security settings: status badge next to the two-factor authentication title when it is turned off. One word.',
|
||||
});
|
||||
const PASSKEY_COUNT_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {# passkey} other {# passkeys}}',
|
||||
comment: 'Security settings: badge next to the Passkeys title showing how many passkeys are registered.',
|
||||
});
|
||||
const TURN_OFF_AUTHENTICATOR_APP_DESCRIPTOR = msg({
|
||||
message: 'Turn off authenticator app',
|
||||
comment:
|
||||
'Security settings: accessible name for the button that removes authenticator-app two-factor authentication.',
|
||||
});
|
||||
const SET_UP_AUTHENTICATOR_APP_DESCRIPTOR = msg({
|
||||
message: 'Set up authenticator app',
|
||||
comment: 'Security settings: accessible name for the button that starts authenticator-app setup.',
|
||||
});
|
||||
const RENAME_PASSKEY_NAMED_DESCRIPTOR = msg({
|
||||
message: 'Rename passkey {passkeyName}',
|
||||
comment:
|
||||
'Security settings: accessible name for the rename button on a passkey row. {passkeyName} is the name the user gave the passkey.',
|
||||
});
|
||||
const DELETE_PASSKEY_NAMED_DESCRIPTOR = msg({
|
||||
message: 'Delete passkey {passkeyName}',
|
||||
comment:
|
||||
'Security settings: accessible name for the delete button on a passkey row. {passkeyName} is the name the user gave the passkey.',
|
||||
});
|
||||
const MANAGE_AUTHORIZED_APPS_DESCRIPTOR = msg({
|
||||
message: 'Manage authorized apps',
|
||||
comment: 'Security settings: accessible name for the button that opens the authorized apps list.',
|
||||
});
|
||||
const MANAGE_DEVICES_DESCRIPTOR = msg({
|
||||
message: 'Manage devices',
|
||||
comment: 'Security settings: accessible name for the button that opens the signed-in devices list.',
|
||||
});
|
||||
const MANAGE_APPS_AND_DEVICES_WITH_ACCESS_TO_YOUR_DESCRIPTOR = msg({
|
||||
message: 'Manage apps and devices with access to your account',
|
||||
@@ -289,47 +340,36 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
</SettingsTabSection>
|
||||
);
|
||||
}
|
||||
const totpDescription = hasTotpMfa ? (
|
||||
<Trans>Codes from your authenticator app are your second step when you sign in.</Trans>
|
||||
) : (
|
||||
<Trans>Use an authenticator app to generate codes for two-factor authentication</Trans>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingsTabSection
|
||||
<AccountSecurityCard
|
||||
title={i18n._(TWO_FACTOR_AUTHENTICATION_DESCRIPTOR)}
|
||||
description={<Trans>Add an extra layer of security to your account</Trans>}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.settings-tab-section--2"
|
||||
description={i18n._(TWO_FACTOR_CARD_DESCRIPTION_DESCRIPTOR)}
|
||||
status={
|
||||
hasAnyMfa
|
||||
? {label: i18n._(STATUS_ON_DESCRIPTOR), tone: 'success'}
|
||||
: {label: i18n._(STATUS_OFF_DESCRIPTOR), tone: 'muted'}
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.two-factor-card"
|
||||
>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.security-tab.security-tab-content.row">
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.label"
|
||||
>
|
||||
<Trans>Authenticator app</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.description}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.description"
|
||||
>
|
||||
{hasTotpMfa ? (
|
||||
<Trans>Two-factor authentication is enabled</Trans>
|
||||
) : (
|
||||
<Trans>Use an authenticator app to generate codes for two-factor authentication</Trans>
|
||||
)}
|
||||
</div>
|
||||
{needsEmailVerification && !hasTotpMfa && (
|
||||
<div
|
||||
className={styles.warningText}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.warning-text"
|
||||
>
|
||||
{i18n._(VERIFY_EMAIL_BEFORE_AUTHENTICATOR_APP_DESCRIPTOR)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Authenticator app</Trans>}
|
||||
description={totpDescription}
|
||||
warning={
|
||||
needsEmailVerification && !hasTotpMfa ? i18n._(VERIFY_EMAIL_BEFORE_AUTHENTICATOR_APP_DESCRIPTOR) : null
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.authenticator-app-row"
|
||||
>
|
||||
{hasTotpMfa ? (
|
||||
<Button
|
||||
variant="danger"
|
||||
variant="secondary"
|
||||
small={true}
|
||||
aria-label={i18n._(TURN_OFF_AUTHENTICATOR_APP_DESCRIPTOR)}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
@@ -339,12 +379,13 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.push"
|
||||
>
|
||||
<Trans>Disable</Trans>
|
||||
<Trans>Turn off</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
small={true}
|
||||
disabled={!canAddSecurityCredential}
|
||||
aria-label={i18n._(SET_UP_AUTHENTICATOR_APP_DESCRIPTOR)}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
@@ -357,93 +398,114 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.push--2"
|
||||
>
|
||||
<Trans>Enable</Trans>
|
||||
<Trans>Set up</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</AccountSecurityRow>
|
||||
{hasAnyMfa && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.divider"
|
||||
<AccountSecurityRow
|
||||
label={<Trans>Backup codes</Trans>}
|
||||
description={<Trans>View and manage your backup codes for account recovery</Trans>}
|
||||
data-flx="user.account-security-tab.security-tab.backup-codes-row"
|
||||
>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.security-tab.security-tab-content.row--2">
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.row-content--2"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.label--2"
|
||||
>
|
||||
<Trans>Backup codes</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={styles.description}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.description--2"
|
||||
>
|
||||
<Trans>View and manage your backup codes for account recovery</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<BackupCodesViewModal
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.backup-codes-view-modal"
|
||||
/>
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.push--3"
|
||||
>
|
||||
<Trans>View codes</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() =>
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<BackupCodesViewModal
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.backup-codes-view-modal"
|
||||
/>
|
||||
)),
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.push--3"
|
||||
>
|
||||
<Trans>View codes</Trans>
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
<SettingsTabSection
|
||||
</AccountSecurityCard>
|
||||
<AccountSecurityCard
|
||||
title={<Trans>Passkeys</Trans>}
|
||||
description={<Trans>Use passkeys to sign in without a password</Trans>}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.settings-tab-section--3"
|
||||
>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.security-tab.security-tab-content.row--3">
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.row-content--3"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.label--3"
|
||||
>
|
||||
<Trans>Registered passkeys</Trans>
|
||||
</div>
|
||||
{needsEmailVerification && (
|
||||
<div
|
||||
className={styles.warningText}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.warning-text--2"
|
||||
>
|
||||
{i18n._(VERIFY_EMAIL_BEFORE_PASSKEY_DESCRIPTOR)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
status={
|
||||
passkeys.length > 0
|
||||
? {
|
||||
label: i18n._(PASSKEY_COUNT_DESCRIPTOR, {count: passkeys.length}),
|
||||
tone: 'neutral',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
action={
|
||||
<Button
|
||||
small={true}
|
||||
variant={passkeys.length > 0 ? 'secondary' : 'primary'}
|
||||
disabled={!canAddPasskey}
|
||||
onClick={handleAddPasskey}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.add-passkey"
|
||||
>
|
||||
<Trans>Add passkey</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.passkeys-card"
|
||||
>
|
||||
{needsEmailVerification && (
|
||||
<AccountSecurityEmptyRow data-flx="user.account-security-tab.security-tab.passkeys-verify-email">
|
||||
{i18n._(VERIFY_EMAIL_BEFORE_PASSKEY_DESCRIPTOR)}
|
||||
</AccountSecurityEmptyRow>
|
||||
)}
|
||||
{passkeys.length === 0 && !needsEmailVerification && (
|
||||
<AccountSecurityEmptyRow data-flx="user.account-security-tab.security-tab.passkeys-empty">
|
||||
<Trans>No passkeys yet. Add one to sign in with your fingerprint, face or a security key.</Trans>
|
||||
</AccountSecurityEmptyRow>
|
||||
)}
|
||||
{passkeys.map((passkey) => {
|
||||
const createdDate = DateUtils.getRelativeDateString(new Date(passkey.created_at), i18n);
|
||||
const lastUsedDate = passkey.last_used_at
|
||||
? DateUtils.getRelativeDateString(new Date(passkey.last_used_at), i18n)
|
||||
: null;
|
||||
const passkeyName = passkey.name;
|
||||
return (
|
||||
<AccountSecurityRow
|
||||
key={passkey.id}
|
||||
label={passkeyName}
|
||||
description={
|
||||
lastUsedDate ? (
|
||||
<Trans>
|
||||
Added {createdDate}, last used {lastUsedDate}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Added {createdDate}, never used</Trans>
|
||||
)
|
||||
}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-item"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
aria-label={i18n._(RENAME_PASSKEY_NAMED_DESCRIPTOR, {passkeyName})}
|
||||
onClick={() => handleRenamePasskey(passkey.id)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.rename-passkey"
|
||||
>
|
||||
<Trans>Rename</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
small={true}
|
||||
aria-label={i18n._(DELETE_PASSKEY_NAMED_DESCRIPTOR, {passkeyName})}
|
||||
onClick={() => handleDeletePasskey(passkey.id)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.delete-passkey"
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
);
|
||||
})}
|
||||
{passkeys.length > 0 && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-two-factor-divider"
|
||||
>
|
||||
<AccountSecuritySwitchRow data-flx="user.account-security-tab.security-tab.passkey-two-factor-row">
|
||||
<Switch
|
||||
label={<Trans>Require a passkey as your second factor</Trans>}
|
||||
description={<Trans>Ask for a passkey after your password when you sign in</Trans>}
|
||||
@@ -451,154 +513,51 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
onChange={handleTogglePasskeyTwoFactor}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.switch.passkey-two-factor"
|
||||
/>
|
||||
</div>
|
||||
</AccountSecuritySwitchRow>
|
||||
)}
|
||||
{passkeys.length > 0 && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.divider--2"
|
||||
>
|
||||
<div
|
||||
className={styles.passkeyList}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-list"
|
||||
>
|
||||
{passkeys.map((passkey) => {
|
||||
const createdDate = DateUtils.getRelativeDateString(new Date(passkey.created_at), i18n);
|
||||
const lastUsedDate = passkey.last_used_at
|
||||
? DateUtils.getRelativeDateString(new Date(passkey.last_used_at), i18n)
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
key={passkey.id}
|
||||
className={styles.passkeyItem}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-item"
|
||||
>
|
||||
<div
|
||||
className={styles.passkeyInfo}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-info"
|
||||
>
|
||||
<div
|
||||
className={styles.passkeyName}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-name"
|
||||
>
|
||||
{passkey.name}
|
||||
</div>
|
||||
<div
|
||||
className={styles.passkeyDetails}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-details"
|
||||
>
|
||||
{lastUsedDate ? (
|
||||
<Trans>
|
||||
Added: {createdDate} • last used: {lastUsedDate}
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Added: {createdDate}</Trans>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.passkeyActions}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-actions"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() => handleRenamePasskey(passkey.id)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.rename-passkey"
|
||||
>
|
||||
<Trans>Rename</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
small={true}
|
||||
onClick={() => handleDeletePasskey(passkey.id)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.button.delete-passkey"
|
||||
>
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
</AccountSecurityCard>
|
||||
{(onManageAuthorizedApps || onManageLinkedDevices) && (
|
||||
<SettingsTabSection
|
||||
title={i18n._(ACCOUNT_ACCESS_DESCRIPTOR)}
|
||||
<AccountSecurityCard
|
||||
title={i18n._(APPS_AND_DEVICES_DESCRIPTOR)}
|
||||
description={i18n._(MANAGE_APPS_AND_DEVICES_WITH_ACCESS_TO_YOUR_DESCRIPTOR)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access"
|
||||
>
|
||||
{onManageAuthorizedApps && (
|
||||
<div
|
||||
className={styles.row}
|
||||
<AccountSecurityRow
|
||||
label={i18n._(AUTHORIZED_APPS_DESCRIPTOR)}
|
||||
description={i18n._(REVIEW_APPS_THAT_CAN_ACCESS_YOUR_ACCOUNT_DESCRIPTOR)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.authorized-apps-row"
|
||||
>
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.authorized-apps-row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.authorized-apps-label"
|
||||
>
|
||||
{i18n._(AUTHORIZED_APPS_DESCRIPTOR)}
|
||||
</div>
|
||||
<div
|
||||
className={styles.description}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.authorized-apps-description"
|
||||
>
|
||||
{i18n._(REVIEW_APPS_THAT_CAN_ACCESS_YOUR_ACCOUNT_DESCRIPTOR)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
submitting={authorizedAppsSubmitting}
|
||||
aria-label={i18n._(MANAGE_AUTHORIZED_APPS_DESCRIPTOR)}
|
||||
onClick={onManageAuthorizedApps}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.button.manage-authorized-apps"
|
||||
>
|
||||
<Trans>Manage</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</AccountSecurityRow>
|
||||
)}
|
||||
{onManageLinkedDevices && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-divider"
|
||||
<AccountSecurityRow
|
||||
label={i18n._(LINKED_DEVICES_DESCRIPTOR)}
|
||||
description={i18n._(REVIEW_SIGNED_IN_DEVICES_DESCRIPTOR)}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-row"
|
||||
>
|
||||
<div
|
||||
className={styles.row}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-row"
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
aria-label={i18n._(MANAGE_DEVICES_DESCRIPTOR)}
|
||||
onClick={onManageLinkedDevices}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.button.manage-linked-devices"
|
||||
>
|
||||
<div
|
||||
className={styles.rowContent}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-row-content"
|
||||
>
|
||||
<div
|
||||
className={styles.label}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-label"
|
||||
>
|
||||
{i18n._(LINKED_DEVICES_DESCRIPTOR)}
|
||||
</div>
|
||||
<div
|
||||
className={styles.description}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.devices-description"
|
||||
>
|
||||
{i18n._(REVIEW_SIGNED_IN_DEVICES_DESCRIPTOR)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
small={true}
|
||||
onClick={onManageLinkedDevices}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.account-access.button.manage-linked-devices"
|
||||
>
|
||||
<Trans>Manage</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Trans>Manage</Trans>
|
||||
</Button>
|
||||
</AccountSecurityRow>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
</AccountSecurityCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user