mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-03-30 12:55:42 +00:00
Confirm account password before adding/removing email addresses
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"collapse": "Collapse",
|
||||
"confirm": "Confirm",
|
||||
"continue": "Continue",
|
||||
"edit": "Edit",
|
||||
"expand": "Expand",
|
||||
@@ -27,6 +28,7 @@
|
||||
"e2ee": "End-to-end encryption",
|
||||
"loading": "Loading…",
|
||||
"next": "Next",
|
||||
"password": "Password",
|
||||
"previous": "Previous",
|
||||
"saved": "Saved",
|
||||
"saving": "Saving…"
|
||||
@@ -57,7 +59,9 @@
|
||||
"email_field_help": "Add an alternative email you can use to access this account.",
|
||||
"email_field_label": "Add email",
|
||||
"email_in_use_error": "The entered email is already in use",
|
||||
"email_invalid_error": "The entered email is invalid"
|
||||
"email_invalid_error": "The entered email is invalid",
|
||||
"incorrect_password_error": "Incorrect password, please try again",
|
||||
"password_confirmation": "Confirm your account password to add this email address"
|
||||
},
|
||||
"browser_session_details": {
|
||||
"current_badge": "Current"
|
||||
@@ -258,7 +262,9 @@
|
||||
"user_email": {
|
||||
"delete_button_confirmation_modal": {
|
||||
"action": "Delete email",
|
||||
"body": "Delete this email?"
|
||||
"body": "Delete this email?",
|
||||
"incorrect_password": "Incorrect password, please try again",
|
||||
"password_confirmation": "Confirm your account password to delete this email address"
|
||||
},
|
||||
"delete_button_title": "Remove email address",
|
||||
"email": "Email"
|
||||
|
||||
105
frontend/src/components/PasswordConfirmation.tsx
Normal file
105
frontend/src/components/PasswordConfirmation.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { Button, Form } from "@vector-im/compound-web";
|
||||
import type React from "react";
|
||||
import { useCallback, useImperativeHandle, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Dialog from "./Dialog";
|
||||
|
||||
type ModalRef = {
|
||||
prompt: () => Promise<string>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
destructive?: boolean;
|
||||
ref: React.Ref<ModalRef>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns a function that prompts the user to enter a password.
|
||||
* The returned function returns a promise that resolves to the password, and
|
||||
* throws an error if the user cancels the prompt.
|
||||
*
|
||||
* It also returns a ref that must be passed to a mounted Modal component.
|
||||
*/
|
||||
export const usePasswordConfirmation = (): [
|
||||
() => Promise<string>,
|
||||
React.RefObject<ModalRef>,
|
||||
] => {
|
||||
const ref = useRef<ModalRef>({
|
||||
prompt: () => {
|
||||
throw new Error("PasswordConfirmationModal is not mounted!");
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = useCallback(() => ref.current.prompt(), []);
|
||||
|
||||
return [prompt, ref] as const;
|
||||
};
|
||||
|
||||
const PasswordConfirmationModal: React.FC<Props> = ({
|
||||
title,
|
||||
destructive,
|
||||
ref,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const resolversRef = useRef<PromiseWithResolvers<string>>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
prompt: () => {
|
||||
setOpen(true);
|
||||
if (resolversRef.current === null) {
|
||||
resolversRef.current = Promise.withResolvers();
|
||||
}
|
||||
return resolversRef.current.promise;
|
||||
},
|
||||
}));
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
resolversRef.current?.reject(new Error("User cancelled password prompt"));
|
||||
resolversRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.currentTarget);
|
||||
const password = data.get("password");
|
||||
if (typeof password !== "string") {
|
||||
throw new Error(); // This should never happen
|
||||
}
|
||||
resolversRef.current?.resolve(password);
|
||||
resolversRef.current = null;
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog.Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
|
||||
<Form.Root onSubmit={onSubmit}>
|
||||
<Form.Field name="password">
|
||||
<Form.Label>{t("common.password")}</Form.Label>
|
||||
<Form.PasswordControl autoFocus autoComplete="current-password" />
|
||||
</Form.Field>
|
||||
|
||||
<Button type="submit" kind="primary" destructive={destructive}>
|
||||
{t("action.confirm")}
|
||||
</Button>
|
||||
</Form.Root>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<Button kind="tertiary">{t("action.cancel")}</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordConfirmationModal;
|
||||
@@ -38,6 +38,7 @@ button[disabled] .user-email-delete-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-4x);
|
||||
border-radius: var(--cpd-space-4x);
|
||||
border: 1px solid var(--cpd-color-gray-400);
|
||||
padding: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-semibold);
|
||||
|
||||
@@ -7,16 +7,25 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import IconDelete from "@vector-im/compound-design-tokens/assets/web/icons/delete";
|
||||
import IconEmail from "@vector-im/compound-design-tokens/assets/web/icons/email";
|
||||
import { Button, Form, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import {
|
||||
Button,
|
||||
ErrorMessage,
|
||||
Form,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@vector-im/compound-web";
|
||||
import { type ReactNode, useCallback, useState } from "react";
|
||||
import { Translation, useTranslation } from "react-i18next";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import { Close, Description, Dialog, Title } from "../Dialog";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
import PasswordConfirmationModal, {
|
||||
usePasswordConfirmation,
|
||||
} from "../PasswordConfirmation";
|
||||
import styles from "./UserEmail.module.css";
|
||||
|
||||
// This component shows a single user email address, with controls to verify it,
|
||||
// resend the verification email, remove it, and set it as the primary email address.
|
||||
// This component shows a single user email address, with controls to remove it
|
||||
|
||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmail_email on UserEmail {
|
||||
@@ -25,15 +34,9 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`);
|
||||
|
||||
const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation RemoveEmail($id: ID!) {
|
||||
removeEmail(input: { userEmailId: $id }) {
|
||||
mutation RemoveEmail($id: ID!, $password: String) {
|
||||
removeEmail(input: { userEmailId: $id, password: $password }) {
|
||||
status
|
||||
|
||||
user {
|
||||
@@ -64,92 +67,135 @@ const DeleteButton: React.FC<{ disabled?: boolean; onClick?: () => void }> = ({
|
||||
</Translation>
|
||||
);
|
||||
|
||||
const DeleteButtonWithConfirmation: React.FC<
|
||||
ComponentProps<typeof DeleteButton> & { email: string }
|
||||
> = ({ email, onClick, ...rest }) => {
|
||||
const { t } = useTranslation();
|
||||
const onConfirm = (): void => {
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
// NOOP function, otherwise we dont render a cancel button
|
||||
const onDeny = (): void => {};
|
||||
|
||||
return (
|
||||
<Dialog trigger={<DeleteButton {...rest} />}>
|
||||
<Title>
|
||||
{t("frontend.user_email.delete_button_confirmation_modal.body")}
|
||||
</Title>
|
||||
<Description className={styles.emailModalBox}>
|
||||
<IconEmail />
|
||||
<div>{email}</div>
|
||||
</Description>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Close asChild>
|
||||
<Button
|
||||
kind="primary"
|
||||
destructive
|
||||
onClick={onConfirm}
|
||||
Icon={IconDelete}
|
||||
>
|
||||
{t("frontend.user_email.delete_button_confirmation_modal.action")}
|
||||
</Button>
|
||||
</Close>
|
||||
<Close asChild>
|
||||
<Button kind="tertiary" onClick={onDeny}>
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
</Close>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const UserEmail: React.FC<{
|
||||
email: FragmentType<typeof FRAGMENT>;
|
||||
canRemove?: boolean;
|
||||
shouldPromptPassword?: boolean;
|
||||
onRemove?: () => void;
|
||||
}> = ({ email, canRemove, onRemove }) => {
|
||||
}> = ({ email, canRemove, shouldPromptPassword, onRemove }) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const data = useFragment(FRAGMENT, email);
|
||||
const queryClient = useQueryClient();
|
||||
const [promptPassword, passwordConfirmationRef] = usePasswordConfirmation();
|
||||
|
||||
const removeEmail = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
graphqlRequest({ query: REMOVE_EMAIL_MUTATION, variables: { id } }),
|
||||
onSuccess: (_data) => {
|
||||
onRemove?.();
|
||||
mutationFn: ({ id, password }: { id: string; password?: string }) =>
|
||||
graphqlRequest({
|
||||
query: REMOVE_EMAIL_MUTATION,
|
||||
variables: { id, password },
|
||||
}),
|
||||
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
|
||||
// Don't close the modal unless the mutation was successful removed (or not found)
|
||||
if (
|
||||
data.removeEmail.status !== "NOT_FOUND" &&
|
||||
data.removeEmail.status !== "REMOVED"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onRemove?.();
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const onRemoveClick = (): void => {
|
||||
removeEmail.mutate(data.id);
|
||||
};
|
||||
const onRemoveClick = useCallback(
|
||||
async (_e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
|
||||
let password = undefined;
|
||||
if (shouldPromptPassword) {
|
||||
password = await promptPassword();
|
||||
}
|
||||
removeEmail.mutate({ id: data.id, password });
|
||||
},
|
||||
[data.id, promptPassword, shouldPromptPassword, removeEmail.mutate],
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
// Don't change the modal state if the mutation is pending
|
||||
if (removeEmail.isPending) return;
|
||||
removeEmail.reset();
|
||||
setOpen(open);
|
||||
},
|
||||
[removeEmail.isPending, removeEmail.reset],
|
||||
);
|
||||
|
||||
const status = removeEmail.data?.removeEmail.status ?? null;
|
||||
|
||||
return (
|
||||
<Form.Root>
|
||||
<Form.Field name="email">
|
||||
<Form.Label>{t("frontend.user_email.email")}</Form.Label>
|
||||
<>
|
||||
<PasswordConfirmationModal
|
||||
title={t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.password_confirmation",
|
||||
)}
|
||||
destructive
|
||||
ref={passwordConfirmationRef}
|
||||
/>
|
||||
<Form.Root>
|
||||
<Form.Field name="email">
|
||||
<Form.Label>{t("frontend.user_email.email")}</Form.Label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Form.TextControl
|
||||
type="email"
|
||||
readOnly
|
||||
value={data.email}
|
||||
className={styles.userEmailField}
|
||||
/>
|
||||
{canRemove && (
|
||||
<DeleteButtonWithConfirmation
|
||||
email={data.email}
|
||||
disabled={removeEmail.isPending}
|
||||
onClick={onRemoveClick}
|
||||
<div className="flex items-center gap-2">
|
||||
<Form.TextControl
|
||||
type="email"
|
||||
readOnly
|
||||
value={data.email}
|
||||
className={styles.userEmailField}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form.Root>
|
||||
{canRemove && (
|
||||
<Dialog
|
||||
trigger={<DeleteButton />}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Title>
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.body",
|
||||
)}
|
||||
</Title>
|
||||
<Description className={styles.emailModalBox}>
|
||||
<IconEmail />
|
||||
<div>{data.email}</div>
|
||||
</Description>
|
||||
|
||||
{status === "INCORRECT_PASSWORD" && (
|
||||
<ErrorMessage>
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.incorrect_password",
|
||||
)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button
|
||||
kind="primary"
|
||||
type="button"
|
||||
destructive
|
||||
onClick={onRemoveClick}
|
||||
disabled={removeEmail.isPending}
|
||||
Icon={removeEmail.isPending ? undefined : IconDelete}
|
||||
>
|
||||
{!!removeEmail.isPending && <LoadingSpinner inline />}
|
||||
{t(
|
||||
"frontend.user_email.delete_button_confirmation_modal.action",
|
||||
)}
|
||||
</Button>
|
||||
<Close asChild>
|
||||
<Button disabled={removeEmail.isPending} kind="tertiary">
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
</Close>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,13 +10,33 @@ import {
|
||||
ErrorMessage,
|
||||
HelpMessage,
|
||||
} from "@vector-im/compound-web";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql } from "../../gql";
|
||||
import { type FragmentType, graphql, useFragment } from "../../gql";
|
||||
import { graphqlRequest } from "../../graphql";
|
||||
import PasswordConfirmationModal, {
|
||||
usePasswordConfirmation,
|
||||
} from "../PasswordConfirmation";
|
||||
|
||||
export const USER_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`);
|
||||
|
||||
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
mutation AddEmail($email: String!, $language: String!) {
|
||||
startEmailAuthentication(input: { email: $email, language: $language }) {
|
||||
mutation AddEmail($email: String!, $password: String, $language: String!) {
|
||||
startEmailAuthentication(input: {
|
||||
email: $email,
|
||||
password: $password,
|
||||
language: $language
|
||||
}) {
|
||||
status
|
||||
violations
|
||||
authentication {
|
||||
@@ -28,14 +48,26 @@ const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
|
||||
|
||||
const AddEmailForm: React.FC<{
|
||||
onAdd: (id: string) => Promise<void>;
|
||||
}> = ({ onAdd }) => {
|
||||
user: FragmentType<typeof USER_FRAGMENT>;
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
}> = ({ user, siteConfig, onAdd }) => {
|
||||
const { hasPassword } = useFragment(USER_FRAGMENT, user);
|
||||
const { passwordLoginEnabled } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
|
||||
const shouldPromptPassword = hasPassword && passwordLoginEnabled;
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [promptPassword, passwordConfirmationRef] = usePasswordConfirmation();
|
||||
const addEmail = useMutation({
|
||||
mutationFn: ({ email, language }: { email: string; language: string }) =>
|
||||
mutationFn: ({
|
||||
email,
|
||||
password,
|
||||
language,
|
||||
}: { email: string; password?: string; language: string }) =>
|
||||
graphqlRequest({
|
||||
query: ADD_EMAIL_MUTATION,
|
||||
variables: { email, language },
|
||||
variables: { email, password, language },
|
||||
}),
|
||||
onSuccess: async (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
|
||||
@@ -54,62 +86,96 @@ const AddEmailForm: React.FC<{
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
await addEmail.mutateAsync({ email, language: i18n.languages[0] });
|
||||
};
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const email = formData.get("input") as string;
|
||||
let password = undefined;
|
||||
if (shouldPromptPassword) {
|
||||
password = await promptPassword();
|
||||
}
|
||||
|
||||
const data = await addEmail.mutateAsync({
|
||||
email,
|
||||
password,
|
||||
language: i18n.languages[0],
|
||||
});
|
||||
|
||||
if (data.startEmailAuthentication.status !== "STARTED") {
|
||||
// This is so that the 'Edit in place' component doesn't show a 'Saved' message
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
[
|
||||
addEmail.mutateAsync,
|
||||
shouldPromptPassword,
|
||||
promptPassword,
|
||||
i18n.languages,
|
||||
],
|
||||
);
|
||||
|
||||
const status = addEmail.data?.startEmailAuthentication.status ?? null;
|
||||
const violations = addEmail.data?.startEmailAuthentication.violations ?? [];
|
||||
|
||||
return (
|
||||
<EditInPlace
|
||||
onSave={handleSubmit}
|
||||
required
|
||||
type="email"
|
||||
serverInvalid={!!status && status !== "STARTED"}
|
||||
label={t("frontend.add_email_form.email_field_label")}
|
||||
helpLabel={t("frontend.add_email_form.email_field_help")}
|
||||
saveButtonLabel={t("action.save")}
|
||||
savingLabel={t("common.saving")}
|
||||
savedLabel={t("common.saved")}
|
||||
cancelButtonLabel={t("action.cancel")}
|
||||
>
|
||||
<ErrorMessage
|
||||
match="typeMismatch"
|
||||
forceMatch={status === "INVALID_EMAIL_ADDRESS"}
|
||||
<>
|
||||
<PasswordConfirmationModal
|
||||
title={t("frontend.add_email_form.password_confirmation")}
|
||||
ref={passwordConfirmationRef}
|
||||
/>
|
||||
<EditInPlace
|
||||
onSave={handleSubmit}
|
||||
required
|
||||
type="email"
|
||||
serverInvalid={!!status && status !== "STARTED"}
|
||||
label={t("frontend.add_email_form.email_field_label")}
|
||||
helpLabel={t("frontend.add_email_form.email_field_help")}
|
||||
saveButtonLabel={t("action.save")}
|
||||
savingLabel={t("common.saving")}
|
||||
savedLabel={t("common.saved")}
|
||||
cancelButtonLabel={t("action.cancel")}
|
||||
>
|
||||
{t("frontend.add_email_form.email_invalid_error")}
|
||||
</ErrorMessage>
|
||||
|
||||
{status === "IN_USE" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_in_use_error")}
|
||||
<ErrorMessage
|
||||
match="typeMismatch"
|
||||
forceMatch={status === "INVALID_EMAIL_ADDRESS"}
|
||||
>
|
||||
{t("frontend.add_email_form.email_invalid_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "RATE_LIMITED" && (
|
||||
<ErrorMessage>{t("frontend.errors.rate_limit_exceeded")}</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "DENIED" && (
|
||||
<>
|
||||
{status === "IN_USE" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_denied_error")}
|
||||
{t("frontend.add_email_form.email_in_use_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{violations.map((violation) => (
|
||||
// XXX: those messages are bad, but it's better to show them than show a generic message
|
||||
<HelpMessage key={violation}>{violation}</HelpMessage>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</EditInPlace>
|
||||
{status === "RATE_LIMITED" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.errors.rate_limit_exceeded")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
{status === "DENIED" && (
|
||||
<>
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.email_denied_error")}
|
||||
</ErrorMessage>
|
||||
|
||||
{violations.map((violation) => (
|
||||
// XXX: those messages are bad, but it's better to show them than show a generic message
|
||||
<HelpMessage key={violation}>{violation}</HelpMessage>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === "INCORRECT_PASSWORD" && (
|
||||
<ErrorMessage>
|
||||
{t("frontend.add_email_form.incorrect_password_error")}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
</EditInPlace>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -60,16 +60,30 @@ export const query = (pagination: AnyPagination = { first: 6 }) =>
|
||||
}),
|
||||
});
|
||||
|
||||
export const USER_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`);
|
||||
|
||||
export const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`);
|
||||
|
||||
const UserEmailList: React.FC<{
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
}> = ({ siteConfig }) => {
|
||||
const { emailChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);
|
||||
user: FragmentType<typeof USER_FRAGMENT>;
|
||||
}> = ({ siteConfig, user }) => {
|
||||
const { emailChangeAllowed, passwordLoginEnabled } = useFragment(
|
||||
CONFIG_FRAGMENT,
|
||||
siteConfig,
|
||||
);
|
||||
const { hasPassword } = useFragment(USER_FRAGMENT, user);
|
||||
const shouldPromptPassword = hasPassword && passwordLoginEnabled;
|
||||
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [pagination, setPagination] = usePagination();
|
||||
@@ -102,6 +116,7 @@ const UserEmailList: React.FC<{
|
||||
email={edge.node}
|
||||
key={edge.cursor}
|
||||
canRemove={canRemove}
|
||||
shouldPromptPassword={shouldPromptPassword}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -33,16 +33,18 @@ type Documents = {
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmail_SiteConfigFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": typeof types.UserGreeting_UserFragmentDoc,
|
||||
"\n fragment UserGreeting_siteConfig on SiteConfig {\n displayNameChangeAllowed\n }\n": typeof types.UserGreeting_SiteConfigFragmentDoc,
|
||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n }\n }\n": typeof types.SetDisplayNameDocument,
|
||||
"\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": typeof types.AddEmailDocument,
|
||||
"\n fragment AddEmailForm_user on User {\n hasPassword\n }\n": typeof types.AddEmailForm_UserFragmentDoc,
|
||||
"\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n": typeof types.AddEmailForm_SiteConfigFragmentDoc,
|
||||
"\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": typeof types.AddEmailDocument,
|
||||
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": typeof types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": typeof types.UserEmailList_UserFragmentDoc,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
|
||||
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
|
||||
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
|
||||
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
|
||||
@@ -82,16 +84,18 @@ const documents: Documents = {
|
||||
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
|
||||
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
|
||||
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc,
|
||||
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc,
|
||||
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
|
||||
"\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc,
|
||||
"\n fragment UserGreeting_siteConfig on SiteConfig {\n displayNameChangeAllowed\n }\n": types.UserGreeting_SiteConfigFragmentDoc,
|
||||
"\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n }\n }\n": types.SetDisplayNameDocument,
|
||||
"\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": types.AddEmailDocument,
|
||||
"\n fragment AddEmailForm_user on User {\n hasPassword\n }\n": types.AddEmailForm_UserFragmentDoc,
|
||||
"\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n": types.AddEmailForm_SiteConfigFragmentDoc,
|
||||
"\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n": types.AddEmailDocument,
|
||||
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": types.UserEmailList_UserFragmentDoc,
|
||||
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
|
||||
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
|
||||
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
|
||||
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
|
||||
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
|
||||
@@ -188,11 +192,7 @@ export function graphql(source: "\n fragment UserEmail_email on UserEmail {\n
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n"): typeof import('./graphql').UserEmail_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').RemoveEmailDocument;
|
||||
export function graphql(source: "\n mutation RemoveEmail($id: ID!, $password: String) {\n removeEmail(input: { userEmailId: $id, password: $password }) {\n status\n\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').RemoveEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -208,7 +208,15 @@ export function graphql(source: "\n mutation SetDisplayName($userId: ID!, $disp
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation AddEmail($email: String!, $language: String!) {\n startEmailAuthentication(input: { email: $email, language: $language }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n"): typeof import('./graphql').AddEmailDocument;
|
||||
export function graphql(source: "\n fragment AddEmailForm_user on User {\n hasPassword\n }\n"): typeof import('./graphql').AddEmailForm_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment AddEmailForm_siteConfig on SiteConfig {\n passwordLoginEnabled\n }\n"): typeof import('./graphql').AddEmailForm_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation AddEmail($email: String!, $password: String, $language: String!) {\n startEmailAuthentication(input: {\n email: $email,\n password: $password,\n language: $language\n }) {\n status\n violations\n authentication {\n id\n }\n }\n }\n"): typeof import('./graphql').AddEmailDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -216,7 +224,11 @@ export function graphql(source: "\n query UserEmailList(\n $first: Int\n
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
export function graphql(source: "\n fragment UserEmailList_user on User {\n hasPassword\n }\n"): typeof import('./graphql').UserEmailList_UserFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n"): typeof import('./graphql').UserEmailList_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -224,7 +236,7 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -1654,10 +1654,9 @@ export type OAuth2Session_DetailFragment = (
|
||||
|
||||
export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_EmailFragment' };
|
||||
|
||||
export type UserEmail_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmail_SiteConfigFragment' };
|
||||
|
||||
export type RemoveEmailMutationVariables = Exact<{
|
||||
id: Scalars['ID']['input'];
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -1675,8 +1674,13 @@ export type SetDisplayNameMutationVariables = Exact<{
|
||||
|
||||
export type SetDisplayNameMutation = { __typename?: 'Mutation', setDisplayName: { __typename?: 'SetDisplayNamePayload', status: SetDisplayNameStatus } };
|
||||
|
||||
export type AddEmailForm_UserFragment = { __typename?: 'User', hasPassword: boolean } & { ' $fragmentName'?: 'AddEmailForm_UserFragment' };
|
||||
|
||||
export type AddEmailForm_SiteConfigFragment = { __typename?: 'SiteConfig', passwordLoginEnabled: boolean } & { ' $fragmentName'?: 'AddEmailForm_SiteConfigFragment' };
|
||||
|
||||
export type AddEmailMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
password?: InputMaybe<Scalars['String']['input']>;
|
||||
language: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -1696,16 +1700,21 @@ export type UserEmailListQuery = { __typename?: 'Query', viewer: { __typename: '
|
||||
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
|
||||
) }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } };
|
||||
|
||||
export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean } & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
export type UserEmailList_UserFragment = { __typename?: 'User', hasPassword: boolean } & { ' $fragmentName'?: 'UserEmailList_UserFragment' };
|
||||
|
||||
export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean } & { ' $fragmentName'?: 'UserEmailList_SiteConfigFragment' };
|
||||
|
||||
export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: string, browserSessions: { __typename?: 'BrowserSessionConnection', totalCount: number } } & { ' $fragmentName'?: 'BrowserSessionsOverview_UserFragment' };
|
||||
|
||||
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', hasPassword: boolean, emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: (
|
||||
export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: (
|
||||
{ __typename?: 'User', hasPassword: boolean, emails: { __typename?: 'UserEmailConnection', totalCount: number } }
|
||||
& { ' $fragmentRefs'?: { 'AddEmailForm_UserFragment': AddEmailForm_UserFragment;'UserEmailList_UserFragment': UserEmailList_UserFragment } }
|
||||
) } | { __typename: 'Oauth2Session' }, siteConfig: (
|
||||
{ __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean }
|
||||
& { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
|
||||
& { ' $fragmentRefs'?: { 'AddEmailForm_SiteConfigFragment': AddEmailForm_SiteConfigFragment;'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
|
||||
) };
|
||||
|
||||
export type BrowserSessionListQueryVariables = Exact<{
|
||||
@@ -2161,11 +2170,6 @@ export const UserEmail_EmailFragmentDoc = new TypedDocumentString(`
|
||||
email
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_email"}) as unknown as TypedDocumentString<UserEmail_EmailFragment, unknown>;
|
||||
export const UserEmail_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserEmail_siteConfig"}) as unknown as TypedDocumentString<UserEmail_SiteConfigFragment, unknown>;
|
||||
export const UserGreeting_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserGreeting_user on User {
|
||||
id
|
||||
@@ -2180,9 +2184,25 @@ export const UserGreeting_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
displayNameChangeAllowed
|
||||
}
|
||||
`, {"fragmentName":"UserGreeting_siteConfig"}) as unknown as TypedDocumentString<UserGreeting_SiteConfigFragment, unknown>;
|
||||
export const AddEmailForm_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`, {"fragmentName":"AddEmailForm_user"}) as unknown as TypedDocumentString<AddEmailForm_UserFragment, unknown>;
|
||||
export const AddEmailForm_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`, {"fragmentName":"AddEmailForm_siteConfig"}) as unknown as TypedDocumentString<AddEmailForm_SiteConfigFragment, unknown>;
|
||||
export const UserEmailList_UserFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_user"}) as unknown as TypedDocumentString<UserEmailList_UserFragment, unknown>;
|
||||
export const UserEmailList_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}
|
||||
`, {"fragmentName":"UserEmailList_siteConfig"}) as unknown as TypedDocumentString<UserEmailList_SiteConfigFragment, unknown>;
|
||||
export const BrowserSessionsOverview_UserFragmentDoc = new TypedDocumentString(`
|
||||
@@ -2257,8 +2277,8 @@ export const EndOAuth2SessionDocument = new TypedDocumentString(`
|
||||
}
|
||||
`) as unknown as TypedDocumentString<EndOAuth2SessionMutation, EndOAuth2SessionMutationVariables>;
|
||||
export const RemoveEmailDocument = new TypedDocumentString(`
|
||||
mutation RemoveEmail($id: ID!) {
|
||||
removeEmail(input: {userEmailId: $id}) {
|
||||
mutation RemoveEmail($id: ID!, $password: String) {
|
||||
removeEmail(input: {userEmailId: $id, password: $password}) {
|
||||
status
|
||||
user {
|
||||
id
|
||||
@@ -2274,8 +2294,10 @@ export const SetDisplayNameDocument = new TypedDocumentString(`
|
||||
}
|
||||
`) as unknown as TypedDocumentString<SetDisplayNameMutation, SetDisplayNameMutationVariables>;
|
||||
export const AddEmailDocument = new TypedDocumentString(`
|
||||
mutation AddEmail($email: String!, $language: String!) {
|
||||
startEmailAuthentication(input: {email: $email, language: $language}) {
|
||||
mutation AddEmail($email: String!, $password: String, $language: String!) {
|
||||
startEmailAuthentication(
|
||||
input: {email: $email, password: $password, language: $language}
|
||||
) {
|
||||
status
|
||||
violations
|
||||
authentication {
|
||||
@@ -2318,6 +2340,8 @@ export const UserProfileDocument = new TypedDocumentString(`
|
||||
... on BrowserSession {
|
||||
id
|
||||
user {
|
||||
...AddEmailForm_user
|
||||
...UserEmailList_user
|
||||
hasPassword
|
||||
emails(first: 0) {
|
||||
totalCount
|
||||
@@ -2328,19 +2352,26 @@ export const UserProfileDocument = new TypedDocumentString(`
|
||||
siteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
...AddEmailForm_siteConfig
|
||||
...UserEmailList_siteConfig
|
||||
...UserEmail_siteConfig
|
||||
...PasswordChange_siteConfig
|
||||
}
|
||||
}
|
||||
fragment PasswordChange_siteConfig on SiteConfig {
|
||||
passwordChangeAllowed
|
||||
}
|
||||
fragment UserEmail_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
fragment AddEmailForm_user on User {
|
||||
hasPassword
|
||||
}
|
||||
fragment AddEmailForm_siteConfig on SiteConfig {
|
||||
passwordLoginEnabled
|
||||
}
|
||||
fragment UserEmailList_user on User {
|
||||
hasPassword
|
||||
}
|
||||
fragment UserEmailList_siteConfig on SiteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
}`) as unknown as TypedDocumentString<UserProfileQuery, UserProfileQueryVariables>;
|
||||
export const BrowserSessionListDocument = new TypedDocumentString(`
|
||||
query BrowserSessionList($first: Int, $after: String, $last: Int, $before: String, $lastActive: DateFilter) {
|
||||
@@ -2875,7 +2906,7 @@ export const mockEndOAuth2SessionMutation = (resolver: GraphQLResponseResolver<E
|
||||
* @example
|
||||
* mockRemoveEmailMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { id } = variables;
|
||||
* const { id, password } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { removeEmail }
|
||||
* })
|
||||
@@ -2919,7 +2950,7 @@ export const mockSetDisplayNameMutation = (resolver: GraphQLResponseResolver<Set
|
||||
* @example
|
||||
* mockAddEmailMutation(
|
||||
* ({ query, variables }) => {
|
||||
* const { email, language } = variables;
|
||||
* const { email, password, language } = variables;
|
||||
* return HttpResponse.json({
|
||||
* data: { startEmailAuthentication }
|
||||
* })
|
||||
|
||||
@@ -85,9 +85,18 @@ function Index(): React.ReactElement {
|
||||
defaultOpen
|
||||
title={t("frontend.account.contact_info")}
|
||||
>
|
||||
<UserEmailList siteConfig={siteConfig} />
|
||||
<UserEmailList
|
||||
user={viewerSession.user}
|
||||
siteConfig={siteConfig}
|
||||
/>
|
||||
|
||||
{siteConfig.emailChangeAllowed && <AddEmailForm onAdd={onAdd} />}
|
||||
{siteConfig.emailChangeAllowed && (
|
||||
<AddEmailForm
|
||||
user={viewerSession.user}
|
||||
siteConfig={siteConfig}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
)}
|
||||
</Collapsible.Section>
|
||||
|
||||
<Separator kind="section" />
|
||||
|
||||
@@ -18,6 +18,8 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
... on BrowserSession {
|
||||
id
|
||||
user {
|
||||
...AddEmailForm_user
|
||||
...UserEmailList_user
|
||||
hasPassword
|
||||
emails(first: 0) {
|
||||
totalCount
|
||||
@@ -29,8 +31,8 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
siteConfig {
|
||||
emailChangeAllowed
|
||||
passwordLoginEnabled
|
||||
...AddEmailForm_siteConfig
|
||||
...UserEmailList_siteConfig
|
||||
...UserEmail_siteConfig
|
||||
...PasswordChange_siteConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import { expect, userEvent, waitFor, within } from "@storybook/test";
|
||||
import i18n from "i18next";
|
||||
import { type GraphQLHandler, HttpResponse } from "msw";
|
||||
import { CONFIG_FRAGMENT as PASSWORD_CHANGE_CONFIG_FRAGMENT } from "../../src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview";
|
||||
import { FRAGMENT as USER_EMAIL_FRAGMENT } from "../../src/components/UserEmail/UserEmail";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_EMAIL_FRAGMENT,
|
||||
} from "../../src/components/UserEmail/UserEmail";
|
||||
import { CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT } from "../../src/components/UserProfile/UserEmailList";
|
||||
CONFIG_FRAGMENT as ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/AddEmailForm";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/UserEmailList";
|
||||
import { makeFragmentData } from "../../src/gql";
|
||||
import {
|
||||
mockUserEmailListQuery,
|
||||
@@ -48,12 +52,26 @@ const userProfileHandler = ({
|
||||
viewerSession: {
|
||||
__typename: "BrowserSession",
|
||||
id: "session-id",
|
||||
user: {
|
||||
hasPassword,
|
||||
emails: {
|
||||
totalCount: emailTotalCount,
|
||||
user: Object.assign(
|
||||
{
|
||||
hasPassword,
|
||||
emails: {
|
||||
totalCount: emailTotalCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword,
|
||||
},
|
||||
ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword,
|
||||
},
|
||||
USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
siteConfig: Object.assign(
|
||||
@@ -64,12 +82,14 @@ const userProfileHandler = ({
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed,
|
||||
passwordLoginEnabled,
|
||||
},
|
||||
USER_EMAIL_CONFIG_FRAGMENT,
|
||||
ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed,
|
||||
passwordLoginEnabled,
|
||||
},
|
||||
USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
),
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
import { HttpResponse } from "msw";
|
||||
import { CONFIG_FRAGMENT as PASSWORD_CHANGE_CONFIG_FRAGMENT } from "../../src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview";
|
||||
import { FRAGMENT as FOOTER_FRAGMENT } from "../../src/components/Footer/Footer";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_EMAIL_FRAGMENT,
|
||||
} from "../../src/components/UserEmail/UserEmail";
|
||||
import { FRAGMENT as USER_EMAIL_FRAGMENT } from "../../src/components/UserEmail/UserEmail";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_GREETING_CONFIG_FRAGMENT,
|
||||
FRAGMENT as USER_GREETING_FRAGMENT,
|
||||
} from "../../src/components/UserGreeting/UserGreeting";
|
||||
import { CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT } from "../../src/components/UserProfile/UserEmailList";
|
||||
import {
|
||||
CONFIG_FRAGMENT as ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/AddEmailForm";
|
||||
import {
|
||||
CONFIG_FRAGMENT as USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
USER_FRAGMENT as USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
} from "../../src/components/UserProfile/UserEmailList";
|
||||
import { makeFragmentData } from "../../src/gql";
|
||||
import {
|
||||
mockCurrentUserGreetingQuery,
|
||||
@@ -90,12 +94,26 @@ export const handlers = [
|
||||
viewerSession: {
|
||||
__typename: "BrowserSession",
|
||||
id: "browser-session-id",
|
||||
user: {
|
||||
hasPassword: true,
|
||||
emails: {
|
||||
totalCount: 1,
|
||||
user: Object.assign(
|
||||
{
|
||||
hasPassword: true,
|
||||
emails: {
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword: true,
|
||||
},
|
||||
ADD_USER_EMAIL_USER_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
hasPassword: true,
|
||||
},
|
||||
USER_EMAIL_LIST_USER_FRAGMENT,
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
siteConfig: Object.assign(
|
||||
@@ -106,12 +124,14 @@ export const handlers = [
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed: true,
|
||||
passwordLoginEnabled: true,
|
||||
},
|
||||
USER_EMAIL_CONFIG_FRAGMENT,
|
||||
ADD_USER_EMAIL_CONFIG_FRAGMENT,
|
||||
),
|
||||
makeFragmentData(
|
||||
{
|
||||
emailChangeAllowed: true,
|
||||
passwordLoginEnabled: true,
|
||||
},
|
||||
USER_EMAIL_LIST_CONFIG_FRAGMENT,
|
||||
),
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
exports[`Account home page > display name edit box > displays an error if the display name is invalid 1`] = `
|
||||
<div
|
||||
aria-describedby="radix-:r72:"
|
||||
aria-labelledby="radix-:r71:"
|
||||
aria-describedby="radix-:r7q:"
|
||||
aria-labelledby="radix-:r7p:"
|
||||
class="_body_9cf7b0"
|
||||
data-state="open"
|
||||
id="radix-:r70:"
|
||||
id="radix-:r7o:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<h2
|
||||
class="_title_9cf7b0"
|
||||
id="radix-:r71:"
|
||||
id="radix-:r7p:"
|
||||
>
|
||||
Edit profile
|
||||
</h2>
|
||||
@@ -40,29 +40,29 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
data-invalid="true"
|
||||
for="radix-:r8c:"
|
||||
for="radix-:r9a:"
|
||||
>
|
||||
Display name
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8"
|
||||
id=":r8d:"
|
||||
id=":r9b:"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:r8j:"
|
||||
aria-describedby="radix-:r9h:"
|
||||
aria-invalid="true"
|
||||
autocomplete="name"
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
data-invalid="true"
|
||||
id="radix-:r8c:"
|
||||
id="radix-:r9a:"
|
||||
name="displayname"
|
||||
title=""
|
||||
type="text"
|
||||
value="Alice"
|
||||
/>
|
||||
<button
|
||||
aria-controls=":r8d:"
|
||||
aria-labelledby=":r8e:"
|
||||
aria-controls=":r9b:"
|
||||
aria-labelledby=":r9c:"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
@@ -82,7 +82,7 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:r8j:"
|
||||
id="radix-:r9h:"
|
||||
>
|
||||
This is what others will see wherever you’re signed in.
|
||||
</span>
|
||||
@@ -92,13 +92,13 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r8k:"
|
||||
for="radix-:r9i:"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:r8k:"
|
||||
id="radix-:r9i:"
|
||||
name="mxid"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -129,7 +129,7 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":r8l:"
|
||||
aria-labelledby=":r9j:"
|
||||
class="_close_9cf7b0"
|
||||
type="button"
|
||||
>
|
||||
@@ -150,18 +150,18 @@ exports[`Account home page > display name edit box > displays an error if the di
|
||||
|
||||
exports[`Account home page > display name edit box > lets edit the display name 1`] = `
|
||||
<div
|
||||
aria-describedby="radix-:r1e:"
|
||||
aria-labelledby="radix-:r1d:"
|
||||
aria-describedby="radix-:r1k:"
|
||||
aria-labelledby="radix-:r1j:"
|
||||
class="_body_9cf7b0"
|
||||
data-state="open"
|
||||
id="radix-:r1c:"
|
||||
id="radix-:r1i:"
|
||||
role="dialog"
|
||||
style="pointer-events: auto;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<h2
|
||||
class="_title_9cf7b0"
|
||||
id="radix-:r1d:"
|
||||
id="radix-:r1j:"
|
||||
>
|
||||
Edit profile
|
||||
</h2>
|
||||
@@ -186,27 +186,27 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r2o:"
|
||||
for="radix-:r34:"
|
||||
>
|
||||
Display name
|
||||
</label>
|
||||
<div
|
||||
class="_container_1s836_8"
|
||||
id=":r2p:"
|
||||
id=":r35:"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:r2v:"
|
||||
aria-describedby="radix-:r3b:"
|
||||
autocomplete="name"
|
||||
class="_control_sqdq4_10 _control_1s836_13"
|
||||
id="radix-:r2o:"
|
||||
id="radix-:r34:"
|
||||
name="displayname"
|
||||
title=""
|
||||
type="text"
|
||||
value="Alice"
|
||||
/>
|
||||
<button
|
||||
aria-controls=":r2p:"
|
||||
aria-labelledby=":r2q:"
|
||||
aria-controls=":r35:"
|
||||
aria-labelledby=":r36:"
|
||||
class="_action_1s836_24"
|
||||
type="button"
|
||||
>
|
||||
@@ -226,7 +226,7 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:r2v:"
|
||||
id="radix-:r3b:"
|
||||
>
|
||||
This is what others will see wherever you’re signed in.
|
||||
</span>
|
||||
@@ -236,13 +236,13 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:r30:"
|
||||
for="radix-:r3c:"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:r30:"
|
||||
id="radix-:r3c:"
|
||||
name="mxid"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -273,7 +273,7 @@ exports[`Account home page > display name edit box > lets edit the display name
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":r31:"
|
||||
aria-labelledby=":r3d:"
|
||||
class="_close_9cf7b0"
|
||||
type="button"
|
||||
>
|
||||
@@ -463,7 +463,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rg:"
|
||||
for="radix-:rj:"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
@@ -472,7 +472,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<input
|
||||
class="_control_sqdq4_10 _userEmailField_e2a518"
|
||||
id="radix-:rg:"
|
||||
id="radix-:rj:"
|
||||
name="email"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -490,7 +490,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rh:"
|
||||
for="radix-:rn:"
|
||||
>
|
||||
Add email
|
||||
</label>
|
||||
@@ -498,9 +498,9 @@ exports[`Account home page > renders the page 1`] = `
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-:ri:"
|
||||
aria-describedby="radix-:ro:"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:rh:"
|
||||
id="radix-:rn:"
|
||||
name="input"
|
||||
required=""
|
||||
title=""
|
||||
@@ -509,7 +509,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
</div>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:ri:"
|
||||
id="radix-:ro:"
|
||||
>
|
||||
Add an alternative email you can use to access this account.
|
||||
</span>
|
||||
@@ -524,7 +524,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
role="separator"
|
||||
/>
|
||||
<section
|
||||
aria-labelledby=":rj:"
|
||||
aria-labelledby=":rp:"
|
||||
class="_root_f1daaa"
|
||||
data-state="open"
|
||||
>
|
||||
@@ -536,14 +536,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<h4
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 _triggerTitle_f1daaa"
|
||||
id=":rj:"
|
||||
id=":rp:"
|
||||
>
|
||||
Account password
|
||||
</h4>
|
||||
<button
|
||||
aria-controls="radix-:rl:"
|
||||
aria-controls="radix-:rr:"
|
||||
aria-expanded="true"
|
||||
aria-labelledby=":rm:"
|
||||
aria-labelledby=":rs:"
|
||||
class="_icon-button_m2erp_8 _triggerIcon_f1daaa"
|
||||
data-state="open"
|
||||
role="button"
|
||||
@@ -573,7 +573,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
<article
|
||||
class="_content_f1daaa"
|
||||
data-state="open"
|
||||
id="radix-:rl:"
|
||||
id="radix-:rr:"
|
||||
style="transition-duration: 0s; animation-name: none;"
|
||||
>
|
||||
<form
|
||||
@@ -584,14 +584,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_19upo_59"
|
||||
for="radix-:rr:"
|
||||
for="radix-:r11:"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="radix-:rs:"
|
||||
aria-describedby="radix-:r12:"
|
||||
class="_control_sqdq4_10"
|
||||
id="radix-:rr:"
|
||||
id="radix-:r11:"
|
||||
name="password_preview"
|
||||
readonly=""
|
||||
title=""
|
||||
@@ -600,7 +600,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
/>
|
||||
<span
|
||||
class="_message_19upo_85 _help-message_19upo_91"
|
||||
id="radix-:rs:"
|
||||
id="radix-:r12:"
|
||||
>
|
||||
<a
|
||||
class="_link_7634c3"
|
||||
@@ -620,7 +620,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
role="separator"
|
||||
/>
|
||||
<section
|
||||
aria-labelledby=":rt:"
|
||||
aria-labelledby=":r13:"
|
||||
class="_root_f1daaa"
|
||||
data-state="closed"
|
||||
>
|
||||
@@ -632,14 +632,14 @@ exports[`Account home page > renders the page 1`] = `
|
||||
>
|
||||
<h4
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93 _triggerTitle_f1daaa"
|
||||
id=":rt:"
|
||||
id=":r13:"
|
||||
>
|
||||
End-to-end encryption
|
||||
</h4>
|
||||
<button
|
||||
aria-controls="radix-:rv:"
|
||||
aria-controls="radix-:r15:"
|
||||
aria-expanded="false"
|
||||
aria-labelledby=":r10:"
|
||||
aria-labelledby=":r16:"
|
||||
class="_icon-button_m2erp_8 _triggerIcon_f1daaa"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
@@ -675,7 +675,7 @@ exports[`Account home page > renders the page 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="radix-:r15:"
|
||||
aria-controls="radix-:r1b:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _destructive_vczzf_107"
|
||||
|
||||
Reference in New Issue
Block a user