Use the new GraphQL APIs in the frontend to add emails

This commit is contained in:
Quentin Gliech
2025-01-10 16:12:53 +01:00
parent a739a78602
commit 0855c3483b
16 changed files with 422 additions and 622 deletions

View File

@@ -49,9 +49,9 @@
},
"add_email_form": {
"email_denied_error": "The entered email is not allowed by the server policy",
"email_exists_error": "The entered email is already added to this account",
"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"
},
"browser_session_details": {

View File

@@ -12,8 +12,8 @@ type Props = {
Icon: React.ComponentType<React.SVGAttributes<SVGElement>>;
invalid?: boolean;
success?: boolean;
title: string;
subtitle?: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
};
const PageHeading: React.FC<Props> = ({

View File

@@ -15,44 +15,42 @@ import { graphql } from "../../gql";
import { graphqlRequest } from "../../graphql";
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation AddEmail($userId: ID!, $email: String!) {
addEmail(input: { userId: $userId, email: $email }) {
mutation AddEmail($email: String!, $language: String!) {
startEmailAuthentication(input: { email: $email, language: $language }) {
status
violations
email {
authentication {
id
...UserEmail_email
}
}
}
`);
const AddEmailForm: React.FC<{
userId: string;
onAdd: (id: string) => Promise<void>;
}> = ({ userId, onAdd }) => {
const { t } = useTranslation();
}> = ({ onAdd }) => {
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
const addEmail = useMutation({
mutationFn: ({ userId, email }: { userId: string; email: string }) =>
mutationFn: ({ email, language }: { email: string; language: string }) =>
graphqlRequest({
query: ADD_EMAIL_MUTATION,
variables: { userId, email },
variables: { email, language },
}),
onSuccess: async (data) => {
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
// Don't clear the form if the email was invalid or already exists
if (data.addEmail.status !== "ADDED") {
if (data.startEmailAuthentication.status !== "STARTED") {
return;
}
if (!data.addEmail.email?.id) {
if (!data.startEmailAuthentication.authentication?.id) {
throw new Error("Unexpected response from server");
}
// Call the onAdd callback
await onAdd(data.addEmail.email?.id);
await onAdd(data.startEmailAuthentication.authentication?.id);
},
});
@@ -63,20 +61,18 @@ const AddEmailForm: React.FC<{
const formData = new FormData(e.currentTarget);
const email = formData.get("input") as string;
addEmail.mutate({ userId, email });
addEmail.mutate({ email, language: i18n.languages[0] });
};
const status = addEmail.data?.addEmail.status ?? null;
const violations = addEmail.data?.addEmail.violations ?? [];
const status = addEmail.data?.startEmailAuthentication.status ?? null;
const violations = addEmail.data?.startEmailAuthentication.violations ?? [];
return (
<EditInPlace
onSave={handleSubmit}
required
type="email"
serverInvalid={
status === "INVALID" || status === "EXISTS" || status === "DENIED"
}
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")}
@@ -84,13 +80,16 @@ const AddEmailForm: React.FC<{
savedLabel={t("common.saved")}
cancelButtonLabel={t("action.cancel")}
>
<ErrorMessage match="typeMismatch" forceMatch={status === "INVALID"}>
<ErrorMessage
match="typeMismatch"
forceMatch={status === "INVALID_EMAIL_ADDRESS"}
>
{t("frontend.add_email_form.email_invalid_error")}
</ErrorMessage>
{status === "EXISTS" && (
{status === "IN_USE" && (
<ErrorMessage>
{t("frontend.add_email_form.email_exists_error")}
{t("frontend.add_email_form.email_in_use_error")}
</ErrorMessage>
)}

View File

@@ -1,32 +0,0 @@
/* Copyright 2024 New Vector Ltd.
* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--cpd-space-2x);
text-align: center;
}
.tagline {
color: var(--cpd-color-text-secondary);
& > span {
color: var(--cpd-color-text-primary);
}
}
.icon {
height: var(--cpd-space-16x);
width: var(--cpd-space-16x);
color: var(--cpd-color-icon-secondary);
background: var(--cpd-color-bg-subtle-secondary);
padding: var(--cpd-space-2x);
border-radius: var(--cpd-space-2x);
margin-bottom: var(--cpd-space-2x);
}

View File

@@ -1,169 +0,0 @@
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useLinkProps, useNavigate } from "@tanstack/react-router";
import IconArrowLeft from "@vector-im/compound-design-tokens/assets/web/icons/arrow-left";
import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send-solid";
import { Alert, Button, Form, H1, Text } from "@vector-im/compound-web";
import { useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../../gql";
import { graphqlRequest } from "../../graphql";
import styles from "./VerifyEmail.module.css";
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_verifyEmail on UserEmail {
id
email
}
`);
const VERIFY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation DoVerifyEmail($id: ID!, $code: String!) {
verifyEmail(input: { userEmailId: $id, code: $code }) {
status
}
}
`);
const RESEND_VERIFICATION_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation ResendVerificationEmail($id: ID!) {
sendVerificationEmail(input: { userEmailId: $id }) {
status
}
}
`);
const BackButton: React.FC = () => {
const props = useLinkProps({ to: "/" });
const { t } = useTranslation();
return (
<Button as="a" Icon={IconArrowLeft} kind="tertiary" {...props}>
{t("action.back")}
</Button>
);
};
const VerifyEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
}> = ({ email }) => {
const data = useFragment(FRAGMENT, email);
const queryClient = useQueryClient();
const verifyEmail = useMutation({
mutationFn: ({ id, code }: { id: string; code: string }) =>
graphqlRequest({ query: VERIFY_EMAIL_MUTATION, variables: { id, code } }),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["currentUserGreeting"] });
queryClient.invalidateQueries({ queryKey: ["userProfile"] });
queryClient.invalidateQueries({ queryKey: ["userEmails"] });
if (data.verifyEmail.status === "VERIFIED") {
navigate({ to: "/" });
}
},
});
const resendVerificationEmail = useMutation({
mutationFn: (id: string) =>
graphqlRequest({
query: RESEND_VERIFICATION_EMAIL_MUTATION,
variables: { id },
}),
onSuccess: () => {
fieldRef.current?.focus();
},
});
const navigate = useNavigate();
const fieldRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const code = formData.get("code") as string;
verifyEmail.mutateAsync({ id: data.id, code }).finally(() => form.reset());
};
const onResendClick = (): void => {
resendVerificationEmail.mutate(data.id);
};
const emailSent =
resendVerificationEmail.data?.sendVerificationEmail.status === "SENT";
const invalidCode = verifyEmail.data?.verifyEmail.status === "INVALID_CODE";
const { email: codeEmail } = data;
return (
<>
<header className={styles.header}>
<IconSend className={styles.icon} />
<H1>{t("frontend.verify_email.heading")}</H1>
<Text size="lg" className={styles.tagline}>
<Trans
i18nKey="frontend.verify_email.enter_code_prompt"
values={{ email: codeEmail }}
components={{ email: <span /> }}
/>
</Text>
</header>
<Form.Root onSubmit={onFormSubmit}>
{emailSent && (
<Alert
type="success"
title={t("frontend.verify_email.email_sent_alert.title")}
>
{t("frontend.verify_email.email_sent_alert.description")}
</Alert>
)}
{invalidCode && (
<Alert
type="critical"
title={t("frontend.verify_email.invalid_code_alert.title")}
>
{t("frontend.verify_email.invalid_code_alert.description")}
</Alert>
)}
<Form.Field
name="code"
serverInvalid={invalidCode}
className="self-center mb-4"
>
<Form.Label>{t("frontend.verify_email.code_field_label")}</Form.Label>
<Form.MFAControl ref={fieldRef} />
{invalidCode && (
<Form.ErrorMessage>
{t("frontend.verify_email.code_field_error")}
</Form.ErrorMessage>
)}
<Form.ErrorMessage match="patternMismatch">
{t("frontend.verify_email.code_field_wrong_shape")}
</Form.ErrorMessage>
</Form.Field>
<Form.Submit type="submit" disabled={verifyEmail.isPending}>
{t("action.continue")}
</Form.Submit>
<Button
type="button"
kind="secondary"
disabled={resendVerificationEmail.isPending}
onClick={onResendClick}
>
{t("frontend.verify_email.resend_code")}
</Button>
<BackButton />
</Form.Root>
</>
);
};
export default VerifyEmail;

View File

@@ -1,179 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<VerifyEmail /> > renders an active session 1`] = `
[
<header
className="_header_07ded5"
>
<svg
className="_icon_07ded5"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.878.878 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
fill="currentColor"
/>
</svg>
<h1
className="_title_07ded5"
>
Verify your email
</h1>
<p
className="_tagline_07ded5"
>
Enter the 6-digit code sent to
<span
className="_email_07ded5"
>
ernie@sesame.st
</span>
</p>
</header>,
<form
className="_root_xzzw3_23 _form_07ded5"
onInvalid={[Function]}
onReset={[Function]}
onSubmit={[Function]}
>
<div
className="_field_xzzw3_32"
>
<label
className="_label_xzzw3_41"
htmlFor="radix-:r0:"
onMouseDown={[Function]}
>
6-digit code
</label>
<input
className="_control_xzzw3_55"
id="radix-:r0:"
inputMode="numeric"
name="code"
onChange={[Function]}
onInvalid={[Function]}
placeholder="xxxxxx"
title=""
type="text"
/>
</div>
<button
className="_button_lls7s_17 _submitButton_07ded5 _submitButton_07ded5"
data-kind="primary"
data-size="lg"
disabled={false}
role="button"
tabIndex={0}
type="submit"
>
Continue
</button>
<button
className="_button_lls7s_17"
data-kind="tertiary"
data-size="lg"
disabled={false}
onClick={[Function]}
role="button"
tabIndex={0}
>
Resend email
</button>
</form>,
]
`;
exports[`<VerifyEmail /> > renders verify screen for email 1`] = `
[
<header
className="_header_07ded5"
>
<svg
className="_icon_07ded5"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.878.878 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
fill="currentColor"
/>
</svg>
<h1
className="_title_07ded5"
>
Verify your email
</h1>
<p
className="_tagline_07ded5"
>
Enter the 6-digit code sent to
<span
className="_email_07ded5"
>
ernie@sesame.st
</span>
</p>
</header>,
<form
className="_root_xzzw3_23 _form_07ded5"
onInvalid={[Function]}
onReset={[Function]}
onSubmit={[Function]}
>
<div
className="_field_xzzw3_32"
>
<label
className="_label_xzzw3_41"
htmlFor="radix-:r0:"
onMouseDown={[Function]}
>
6-digit code
</label>
<input
className="_control_xzzw3_55"
id="radix-:r0:"
inputMode="numeric"
name="code"
onChange={[Function]}
onInvalid={[Function]}
placeholder="xxxxxx"
title=""
type="text"
/>
</div>
<button
className="_button_lls7s_17 _submitButton_07ded5 _submitButton_07ded5"
data-kind="primary"
data-size="lg"
disabled={false}
role="button"
tabIndex={0}
type="submit"
>
Continue
</button>
<button
className="_button_lls7s_17"
data-kind="tertiary"
data-size="lg"
disabled={false}
onClick={[Function]}
role="button"
tabIndex={0}
>
Resend email
</button>
</form>,
]
`;

View File

@@ -1,7 +0,0 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
export { default } from "./VerifyEmail";

View File

@@ -35,14 +35,11 @@ const documents = {
"\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($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.AddEmailDocument,
"\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 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 BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n": types.UserEmail_VerifyEmailFragmentDoc,
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n }\n }\n": types.DoVerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n }\n }\n": types.ResendVerificationEmailDocument,
"\n query UserProfile {\n viewer {\n __typename\n ... on User {\n id\n\n emails(first: 0) {\n totalCount\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 viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\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 SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument,
"\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,
@@ -51,7 +48,9 @@ const documents = {
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerDocument,
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectDocument,
"\n query VerifyEmail($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n": types.VerifyEmailDocument,
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": types.DoVerifyEmailDocument,
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": types.ResendEmailAuthenticationCodeDocument,
"\n query VerifyEmail($id: ID!) {\n userEmailAuthentication(id: $id) {\n id\n email\n completedAt\n }\n }\n": types.VerifyEmailDocument,
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": types.ChangePasswordDocument,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeDocument,
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": types.RecoverPasswordDocument,
@@ -145,7 +144,7 @@ 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($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"): typeof import('./graphql').AddEmailDocument;
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;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -161,19 +160,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 fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n"): typeof import('./graphql').UserEmail_VerifyEmailFragmentDoc;
/**
* 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 DoVerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n }\n }\n"): typeof import('./graphql').DoVerifyEmailDocument;
/**
* 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 ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n }\n }\n"): typeof import('./graphql').ResendVerificationEmailDocument;
/**
* 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 viewer {\n __typename\n ... on User {\n id\n\n emails(first: 0) {\n totalCount\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 viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\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;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -209,7 +196,15 @@ export function graphql(source: "\n query DeviceRedirect($deviceId: String!, $u
/**
* 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 VerifyEmail($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"): typeof import('./graphql').VerifyEmailDocument;
export function graphql(source: "\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n"): typeof import('./graphql').DoVerifyEmailDocument;
/**
* 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 ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n"): typeof import('./graphql').ResendEmailAuthenticationCodeDocument;
/**
* 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 VerifyEmail($id: ID!) {\n userEmailAuthentication(id: $id) {\n id\n email\n completedAt\n }\n }\n"): typeof import('./graphql').VerifyEmailDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -326,6 +326,30 @@ export type CompatSsoLoginEdge = {
node: CompatSsoLogin;
};
/** The input for the `completeEmailAuthentication` mutation */
export type CompleteEmailAuthenticationInput = {
/** The authentication code to use */
code: Scalars['String']['input'];
/** The ID of the authentication session to complete */
id: Scalars['ID']['input'];
};
/** The payload of the `completeEmailAuthentication` mutation */
export type CompleteEmailAuthenticationPayload = {
__typename?: 'CompleteEmailAuthenticationPayload';
/** Status of the operation */
status: CompleteEmailAuthenticationStatus;
};
/** The status of the `completeEmailAuthentication` mutation */
export type CompleteEmailAuthenticationStatus =
/** The authentication code has expired */
| 'CODE_EXPIRED'
/** The authentication was completed */
| 'COMPLETED'
/** The authentication code is invalid */
| 'INVALID_CODE';
/** The input of the `createOauth2Session` mutation. */
export type CreateOAuth2SessionInput = {
/** Whether the session should issue a never-expiring access token */
@@ -474,12 +498,17 @@ export type MatrixUser = {
/** The mutations root of the GraphQL interface. */
export type Mutation = {
__typename?: 'Mutation';
/** Add an email address to the specified user */
/**
* Add an email address to the specified user
* @deprecated Use `startEmailAuthentication` instead.
*/
addEmail: AddEmailPayload;
/** Add a user. This is only available to administrators. */
addUser: AddUserPayload;
/** Temporarily allow user to reset their cross-signing keys. */
allowUserCrossSigningReset: AllowUserCrossSigningResetPayload;
/** Complete the email authentication flow */
completeEmailAuthentication: CompleteEmailAuthenticationPayload;
/**
* Create a new arbitrary OAuth 2.0 Session.
*
@@ -493,6 +522,8 @@ export type Mutation = {
lockUser: LockUserPayload;
/** Remove an email address */
removeEmail: RemoveEmailPayload;
/** Resend the email authentication code */
resendEmailAuthenticationCode: ResendEmailAuthenticationCodePayload;
/**
* Resend a user recovery email
*
@@ -501,8 +532,6 @@ export type Mutation = {
* calls this mutation.
*/
resendRecoveryEmail: ResendRecoveryEmailPayload;
/** Send a verification code for an email address */
sendVerificationEmail: SendVerificationEmailPayload;
/**
* Set whether a user can request admin. This is only available to
* administrators.
@@ -526,10 +555,10 @@ export type Mutation = {
* @deprecated This doesn't do anything anymore, but is kept to avoid breaking existing queries
*/
setPrimaryEmail: SetPrimaryEmailPayload;
/** Start a new email authentication flow */
startEmailAuthentication: StartEmailAuthenticationPayload;
/** Unlock a user. This is only available to administrators. */
unlockUser: UnlockUserPayload;
/** Submit a verification code for an email address */
verifyEmail: VerifyEmailPayload;
};
@@ -551,6 +580,12 @@ export type MutationAllowUserCrossSigningResetArgs = {
};
/** The mutations root of the GraphQL interface. */
export type MutationCompleteEmailAuthenticationArgs = {
input: CompleteEmailAuthenticationInput;
};
/** The mutations root of the GraphQL interface. */
export type MutationCreateOauth2SessionArgs = {
input: CreateOAuth2SessionInput;
@@ -588,14 +623,14 @@ export type MutationRemoveEmailArgs = {
/** The mutations root of the GraphQL interface. */
export type MutationResendRecoveryEmailArgs = {
input: ResendRecoveryEmailInput;
export type MutationResendEmailAuthenticationCodeArgs = {
input: ResendEmailAuthenticationCodeInput;
};
/** The mutations root of the GraphQL interface. */
export type MutationSendVerificationEmailArgs = {
input: SendVerificationEmailInput;
export type MutationResendRecoveryEmailArgs = {
input: ResendRecoveryEmailInput;
};
@@ -630,14 +665,14 @@ export type MutationSetPrimaryEmailArgs = {
/** The mutations root of the GraphQL interface. */
export type MutationUnlockUserArgs = {
input: UnlockUserInput;
export type MutationStartEmailAuthenticationArgs = {
input: StartEmailAuthenticationInput;
};
/** The mutations root of the GraphQL interface. */
export type MutationVerifyEmailArgs = {
input: VerifyEmailInput;
export type MutationUnlockUserArgs = {
input: UnlockUserInput;
};
/** An object with an ID. */
@@ -779,6 +814,8 @@ export type Query = {
userByUsername?: Maybe<User>;
/** Fetch a user email by its ID. */
userEmail?: Maybe<UserEmail>;
/** Fetch a user email authentication session */
userEmailAuthentication?: Maybe<UserEmailAuthentication>;
/** Fetch a user recovery ticket. */
userRecoveryTicket?: Maybe<UserRecoveryTicket>;
/**
@@ -870,6 +907,12 @@ export type QueryUserEmailArgs = {
};
/** The query root of the GraphQL interface. */
export type QueryUserEmailAuthenticationArgs = {
id: Scalars['ID']['input'];
};
/** The query root of the GraphQL interface. */
export type QueryUserRecoveryTicketArgs = {
ticket: Scalars['String']['input'];
@@ -910,6 +953,28 @@ export type RemoveEmailStatus =
/** The email address was removed */
| 'REMOVED';
/** The input for the `resendEmailAuthenticationCode` mutation */
export type ResendEmailAuthenticationCodeInput = {
/** The ID of the authentication session to resend the code for */
id: Scalars['ID']['input'];
/** The language to use for the email */
language?: Scalars['String']['input'];
};
/** The payload of the `resendEmailAuthenticationCode` mutation */
export type ResendEmailAuthenticationCodePayload = {
__typename?: 'ResendEmailAuthenticationCodePayload';
/** Status of the operation */
status: ResendEmailAuthenticationCodeStatus;
};
/** The status of the `resendEmailAuthenticationCode` mutation */
export type ResendEmailAuthenticationCodeStatus =
/** The email authentication session is already completed */
| 'COMPLETED'
/** The email was resent */
| 'RESENT';
/** The input for the `resendRecoveryEmail` mutation. */
export type ResendRecoveryEmailInput = {
/** The recovery ticket to use. */
@@ -934,28 +999,6 @@ export type ResendRecoveryEmailStatus =
/** The recovery email was sent. */
| 'SENT';
/** The input for the `sendVerificationEmail` mutation */
export type SendVerificationEmailInput = {
/** The ID of the email address to verify */
userEmailId: Scalars['ID']['input'];
};
/** The payload of the `sendVerificationEmail` mutation */
export type SendVerificationEmailPayload = {
__typename?: 'SendVerificationEmailPayload';
/** The email address to which the verification email was sent */
email: UserEmail;
/** Status of the operation */
status: SendVerificationEmailStatus;
/** The user to whom the email address belongs */
user: User;
};
/** The status of the `sendVerificationEmail` mutation */
export type SendVerificationEmailStatus =
/** The email address is already verified */
| 'ALREADY_VERIFIED';
/** A client session, either compat or OAuth 2.0 */
export type Session = CompatSession | Oauth2Session;
@@ -1135,6 +1178,36 @@ export type SiteConfig = Node & {
tosUri?: Maybe<Scalars['Url']['output']>;
};
/** The input for the `startEmailAuthentication` mutation */
export type StartEmailAuthenticationInput = {
/** The email address to add to the account */
email: Scalars['String']['input'];
/** The language to use for the email */
language?: Scalars['String']['input'];
};
/** The payload of the `startEmailAuthentication` mutation */
export type StartEmailAuthenticationPayload = {
__typename?: 'StartEmailAuthenticationPayload';
/** The email authentication session that was started */
authentication?: Maybe<UserEmailAuthentication>;
/** Status of the operation */
status: StartEmailAuthenticationStatus;
/** The list of policy violations if the email address was denied */
violations?: Maybe<Array<Scalars['String']['output']>>;
};
/** The status of the `startEmailAuthentication` mutation */
export type StartEmailAuthenticationStatus =
/** The email address isn't allowed by the policy */
| 'DENIED'
/** The email address is invalid */
| 'INVALID_EMAIL_ADDRESS'
/** The email address is already in use */
| 'IN_USE'
/** The email address was started */
| 'STARTED';
/** The input for the `unlockUser` mutation. */
export type UnlockUserInput = {
/** The ID of the user to unlock */
@@ -1404,6 +1477,19 @@ export type UserEmail = CreationEvent & Node & {
id: Scalars['ID']['output'];
};
/** A email authentication session */
export type UserEmailAuthentication = CreationEvent & Node & {
__typename?: 'UserEmailAuthentication';
/** When the object was last updated. */
completedAt?: Maybe<Scalars['DateTime']['output']>;
/** When the object was created. */
createdAt: Scalars['DateTime']['output'];
/** The email address associated with this session */
email: Scalars['String']['output'];
/** ID of the object. */
id: Scalars['ID']['output'];
};
export type UserEmailConnection = {
__typename?: 'UserEmailConnection';
/** A list of edges. */
@@ -1463,30 +1549,6 @@ export type UserState =
/** The user is locked. */
| 'LOCKED';
/** The input for the `verifyEmail` mutation */
export type VerifyEmailInput = {
/** The verification code */
code: Scalars['String']['input'];
/** The ID of the email address to verify */
userEmailId: Scalars['ID']['input'];
};
/** The payload of the `verifyEmail` mutation */
export type VerifyEmailPayload = {
__typename?: 'VerifyEmailPayload';
/** The email address that was verified */
email?: Maybe<UserEmail>;
/** Status of the operation */
status: VerifyEmailStatus;
/** The user to whom the email address belongs */
user?: Maybe<User>;
};
/** The status of the `verifyEmail` mutation */
export type VerifyEmailStatus =
/** The email address was already verified before */
| 'ALREADY_VERIFIED';
/** Represents the current viewer */
export type Viewer = Anonymous | User;
@@ -1569,15 +1631,12 @@ export type SetDisplayNameMutationVariables = Exact<{
export type SetDisplayNameMutation = { __typename?: 'Mutation', setDisplayName: { __typename?: 'SetDisplayNamePayload', status: SetDisplayNameStatus } };
export type AddEmailMutationVariables = Exact<{
userId: Scalars['ID']['input'];
email: Scalars['String']['input'];
language: Scalars['String']['input'];
}>;
export type AddEmailMutation = { __typename?: 'Mutation', addEmail: { __typename?: 'AddEmailPayload', status: AddEmailStatus, violations?: Array<string> | null, email?: (
{ __typename?: 'UserEmail', id: string }
& { ' $fragmentRefs'?: { 'UserEmail_EmailFragment': UserEmail_EmailFragment } }
) | null } };
export type AddEmailMutation = { __typename?: 'Mutation', startEmailAuthentication: { __typename?: 'StartEmailAuthenticationPayload', status: StartEmailAuthenticationStatus, violations?: Array<string> | null, authentication?: { __typename?: 'UserEmailAuthentication', id: string } | null } };
export type UserEmailListQueryVariables = Exact<{
first?: InputMaybe<Scalars['Int']['input']>;
@@ -1596,27 +1655,10 @@ export type UserEmailList_SiteConfigFragment = { __typename?: 'SiteConfig', emai
export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: string, browserSessions: { __typename?: 'BrowserSessionConnection', totalCount: number } } & { ' $fragmentName'?: 'BrowserSessionsOverview_UserFragment' };
export type UserEmail_VerifyEmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_VerifyEmailFragment' };
export type DoVerifyEmailMutationVariables = Exact<{
id: Scalars['ID']['input'];
code: Scalars['String']['input'];
}>;
export type DoVerifyEmailMutation = { __typename?: 'Mutation', verifyEmail: { __typename?: 'VerifyEmailPayload', status: VerifyEmailStatus } };
export type ResendVerificationEmailMutationVariables = Exact<{
id: Scalars['ID']['input'];
}>;
export type ResendVerificationEmailMutation = { __typename?: 'Mutation', sendVerificationEmail: { __typename?: 'SendVerificationEmailPayload', status: SendVerificationEmailStatus } };
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', id: string, emails: { __typename?: 'UserEmailConnection', totalCount: number } }, siteConfig: (
export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } }, siteConfig: (
{ __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean }
& { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
) };
@@ -1635,7 +1677,7 @@ export type SessionDetailQuery = { __typename?: 'Query', viewerSession: { __type
) | { __typename: 'CompatSsoLogin', id: string } | { __typename: 'Oauth2Client', id: string } | (
{ __typename: 'Oauth2Session', id: string }
& { ' $fragmentRefs'?: { 'OAuth2Session_DetailFragment': OAuth2Session_DetailFragment } }
) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null };
) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserEmailAuthentication', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null };
export type BrowserSessionListQueryVariables = Exact<{
first?: InputMaybe<Scalars['Int']['input']>;
@@ -1710,15 +1752,28 @@ export type DeviceRedirectQueryVariables = Exact<{
export type DeviceRedirectQuery = { __typename?: 'Query', session?: { __typename: 'CompatSession', id: string } | { __typename: 'Oauth2Session', id: string } | null };
export type DoVerifyEmailMutationVariables = Exact<{
id: Scalars['ID']['input'];
code: Scalars['String']['input'];
}>;
export type DoVerifyEmailMutation = { __typename?: 'Mutation', completeEmailAuthentication: { __typename?: 'CompleteEmailAuthenticationPayload', status: CompleteEmailAuthenticationStatus } };
export type ResendEmailAuthenticationCodeMutationVariables = Exact<{
id: Scalars['ID']['input'];
language: Scalars['String']['input'];
}>;
export type ResendEmailAuthenticationCodeMutation = { __typename?: 'Mutation', resendEmailAuthenticationCode: { __typename?: 'ResendEmailAuthenticationCodePayload', status: ResendEmailAuthenticationCodeStatus } };
export type VerifyEmailQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
export type VerifyEmailQuery = { __typename?: 'Query', userEmail?: (
{ __typename?: 'UserEmail' }
& { ' $fragmentRefs'?: { 'UserEmail_VerifyEmailFragment': UserEmail_VerifyEmailFragment } }
) | null };
export type VerifyEmailQuery = { __typename?: 'Query', userEmailAuthentication?: { __typename?: 'UserEmailAuthentication', id: string, email: string, completedAt?: string | null } | null };
export type ChangePasswordMutationVariables = Exact<{
userId: Scalars['ID']['input'];
@@ -1977,12 +2032,6 @@ export const BrowserSessionsOverview_UserFragmentDoc = new TypedDocumentString(`
}
}
`, {"fragmentName":"BrowserSessionsOverview_user"}) as unknown as TypedDocumentString<BrowserSessionsOverview_UserFragment, unknown>;
export const UserEmail_VerifyEmailFragmentDoc = new TypedDocumentString(`
fragment UserEmail_verifyEmail on UserEmail {
id
email
}
`, {"fragmentName":"UserEmail_verifyEmail"}) as unknown as TypedDocumentString<UserEmail_VerifyEmailFragment, unknown>;
export const RecoverPassword_UserRecoveryTicketFragmentDoc = new TypedDocumentString(`
fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {
username
@@ -2082,20 +2131,16 @@ export const SetDisplayNameDocument = new TypedDocumentString(`
}
`) as unknown as TypedDocumentString<SetDisplayNameMutation, SetDisplayNameMutationVariables>;
export const AddEmailDocument = new TypedDocumentString(`
mutation AddEmail($userId: ID!, $email: String!) {
addEmail(input: {userId: $userId, email: $email}) {
mutation AddEmail($email: String!, $language: String!) {
startEmailAuthentication(input: {email: $email, language: $language}) {
status
violations
email {
authentication {
id
...UserEmail_email
}
}
}
fragment UserEmail_email on UserEmail {
id
email
}`) as unknown as TypedDocumentString<AddEmailMutation, AddEmailMutationVariables>;
`) as unknown as TypedDocumentString<AddEmailMutation, AddEmailMutationVariables>;
export const UserEmailListDocument = new TypedDocumentString(`
query UserEmailList($first: Int, $after: String, $last: Int, $before: String) {
viewer {
@@ -2123,26 +2168,11 @@ export const UserEmailListDocument = new TypedDocumentString(`
id
email
}`) as unknown as TypedDocumentString<UserEmailListQuery, UserEmailListQueryVariables>;
export const DoVerifyEmailDocument = new TypedDocumentString(`
mutation DoVerifyEmail($id: ID!, $code: String!) {
verifyEmail(input: {userEmailId: $id, code: $code}) {
status
}
}
`) as unknown as TypedDocumentString<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>;
export const ResendVerificationEmailDocument = new TypedDocumentString(`
mutation ResendVerificationEmail($id: ID!) {
sendVerificationEmail(input: {userEmailId: $id}) {
status
}
}
`) as unknown as TypedDocumentString<ResendVerificationEmailMutation, ResendVerificationEmailMutationVariables>;
export const UserProfileDocument = new TypedDocumentString(`
query UserProfile {
viewer {
__typename
... on User {
id
emails(first: 0) {
totalCount
}
@@ -2434,16 +2464,29 @@ export const DeviceRedirectDocument = new TypedDocumentString(`
}
}
`) as unknown as TypedDocumentString<DeviceRedirectQuery, DeviceRedirectQueryVariables>;
export const VerifyEmailDocument = new TypedDocumentString(`
query VerifyEmail($id: ID!) {
userEmail(id: $id) {
...UserEmail_verifyEmail
export const DoVerifyEmailDocument = new TypedDocumentString(`
mutation DoVerifyEmail($id: ID!, $code: String!) {
completeEmailAuthentication(input: {id: $id, code: $code}) {
status
}
}
fragment UserEmail_verifyEmail on UserEmail {
id
email
}`) as unknown as TypedDocumentString<VerifyEmailQuery, VerifyEmailQueryVariables>;
`) as unknown as TypedDocumentString<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>;
export const ResendEmailAuthenticationCodeDocument = new TypedDocumentString(`
mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {
resendEmailAuthenticationCode(input: {id: $id, language: $language}) {
status
}
}
`) as unknown as TypedDocumentString<ResendEmailAuthenticationCodeMutation, ResendEmailAuthenticationCodeMutationVariables>;
export const VerifyEmailDocument = new TypedDocumentString(`
query VerifyEmail($id: ID!) {
userEmailAuthentication(id: $id) {
id
email
completedAt
}
}
`) as unknown as TypedDocumentString<VerifyEmailQuery, VerifyEmailQueryVariables>;
export const ChangePasswordDocument = new TypedDocumentString(`
mutation ChangePassword($userId: ID!, $oldPassword: String!, $newPassword: String!) {
setPassword(
@@ -2653,9 +2696,9 @@ export const mockSetDisplayNameMutation = (resolver: GraphQLResponseResolver<Set
* @example
* mockAddEmailMutation(
* ({ query, variables }) => {
* const { userId, email } = variables;
* const { email, language } = variables;
* return HttpResponse.json({
* data: { addEmail }
* data: { startEmailAuthentication }
* })
* },
* requestOptions
@@ -2690,50 +2733,6 @@ export const mockUserEmailListQuery = (resolver: GraphQLResponseResolver<UserEma
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
* @see https://mswjs.io/docs/basics/response-resolver
* @example
* mockDoVerifyEmailMutation(
* ({ query, variables }) => {
* const { id, code } = variables;
* return HttpResponse.json({
* data: { verifyEmail }
* })
* },
* requestOptions
* )
*/
export const mockDoVerifyEmailMutation = (resolver: GraphQLResponseResolver<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>, options?: RequestHandlerOptions) =>
graphql.mutation<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>(
'DoVerifyEmail',
resolver,
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
* @see https://mswjs.io/docs/basics/response-resolver
* @example
* mockResendVerificationEmailMutation(
* ({ query, variables }) => {
* const { id } = variables;
* return HttpResponse.json({
* data: { sendVerificationEmail }
* })
* },
* requestOptions
* )
*/
export const mockResendVerificationEmailMutation = (resolver: GraphQLResponseResolver<ResendVerificationEmailMutation, ResendVerificationEmailMutationVariables>, options?: RequestHandlerOptions) =>
graphql.mutation<ResendVerificationEmailMutation, ResendVerificationEmailMutationVariables>(
'ResendVerificationEmail',
resolver,
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
@@ -2928,6 +2927,50 @@ export const mockDeviceRedirectQuery = (resolver: GraphQLResponseResolver<Device
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
* @see https://mswjs.io/docs/basics/response-resolver
* @example
* mockDoVerifyEmailMutation(
* ({ query, variables }) => {
* const { id, code } = variables;
* return HttpResponse.json({
* data: { completeEmailAuthentication }
* })
* },
* requestOptions
* )
*/
export const mockDoVerifyEmailMutation = (resolver: GraphQLResponseResolver<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>, options?: RequestHandlerOptions) =>
graphql.mutation<DoVerifyEmailMutation, DoVerifyEmailMutationVariables>(
'DoVerifyEmail',
resolver,
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
* @see https://mswjs.io/docs/basics/response-resolver
* @example
* mockResendEmailAuthenticationCodeMutation(
* ({ query, variables }) => {
* const { id, language } = variables;
* return HttpResponse.json({
* data: { resendEmailAuthenticationCode }
* })
* },
* requestOptions
* )
*/
export const mockResendEmailAuthenticationCodeMutation = (resolver: GraphQLResponseResolver<ResendEmailAuthenticationCodeMutation, ResendEmailAuthenticationCodeMutationVariables>, options?: RequestHandlerOptions) =>
graphql.mutation<ResendEmailAuthenticationCodeMutation, ResendEmailAuthenticationCodeMutationVariables>(
'ResendEmailAuthenticationCode',
resolver,
options
)
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
@@ -2937,7 +2980,7 @@ export const mockDeviceRedirectQuery = (resolver: GraphQLResponseResolver<Device
* ({ query, variables }) => {
* const { id } = variables;
* return HttpResponse.json({
* data: { userEmail }
* data: { userEmailAuthentication }
* })
* },
* requestOptions

View File

@@ -50,9 +50,7 @@ function Index(): React.ReactElement {
>
<UserEmailList siteConfig={siteConfig} />
{siteConfig.emailChangeAllowed && (
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
)}
{siteConfig.emailChangeAllowed && <AddEmailForm onAdd={onAdd} />}
</Collapsible.Section>
<Separator kind="section" />

View File

@@ -17,8 +17,6 @@ const QUERY = graphql(/* GraphQL */ `
viewer {
__typename
... on User {
id
emails(first: 0) {
totalCount
}

View File

@@ -1,17 +1,42 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
import IconArrowLeft from "@vector-im/compound-design-tokens/assets/web/icons/arrow-left";
import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send-solid";
import { Alert, Button, Form } from "@vector-im/compound-web";
import { useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { ButtonLink } from "../components/ButtonLink";
import Layout from "../components/Layout";
import VerifyEmailComponent from "../components/VerifyEmail";
import LoadingSpinner from "../components/LoadingSpinner";
import PageHeading from "../components/PageHeading";
import { graphql } from "../gql";
import { graphqlRequest } from "../graphql";
import { query } from "./emails.$id.verify";
const VERIFY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation DoVerifyEmail($id: ID!, $code: String!) {
completeEmailAuthentication(input: { id: $id, code: $code }) {
status
}
}
`);
const RESEND_EMAIL_AUTHENTICATION_CODE_MUTATION = graphql(/* GraphQL */ `
mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {
resendEmailAuthenticationCode(input: { id: $id, language: $language }) {
status
}
}
`);
export const Route = createLazyFileRoute("/emails/$id/verify")({
component: EmailVerify,
});
@@ -19,13 +44,133 @@ export const Route = createLazyFileRoute("/emails/$id/verify")({
function EmailVerify(): React.ReactElement {
const { id } = Route.useParams();
const {
data: { userEmail },
data: { userEmailAuthentication },
} = useSuspenseQuery(query(id));
if (!userEmail) throw notFound();
if (!userEmailAuthentication) throw notFound();
const queryClient = useQueryClient();
const navigate = useNavigate();
const verifyEmail = useMutation({
mutationFn: ({ id, code }: { id: string; code: string }) =>
graphqlRequest({ query: VERIFY_EMAIL_MUTATION, variables: { id, code } }),
async onSuccess(data): Promise<void> {
await queryClient.invalidateQueries({ queryKey: ["userEmails"] });
await queryClient.invalidateQueries({ queryKey: ["verifyEmail", id] });
if (data.completeEmailAuthentication.status === "COMPLETED") {
await navigate({ to: "/" });
}
},
});
const resendEmailAuthenticationCode = useMutation({
mutationFn: ({ id, language }: { id: string; language: string }) =>
graphqlRequest({
query: RESEND_EMAIL_AUTHENTICATION_CODE_MUTATION,
variables: { id, language },
}),
onSuccess() {
fieldRef.current?.focus();
},
});
const fieldRef = useRef<HTMLInputElement>(null);
const { t, i18n } = useTranslation();
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const code = formData.get("code") as string;
verifyEmail
.mutateAsync({ id: userEmailAuthentication.id, code })
.finally(() => form.reset());
};
const onResendClick = (): void => {
resendEmailAuthenticationCode.mutate({
id: userEmailAuthentication.id,
language: i18n.languages[0],
});
};
const emailSent =
resendEmailAuthenticationCode.data?.resendEmailAuthenticationCode.status ===
"RESENT";
const invalidCode =
verifyEmail.data?.completeEmailAuthentication.status === "INVALID_CODE";
return (
<Layout>
<VerifyEmailComponent email={userEmail} />
<PageHeading
Icon={IconSend}
title={t("frontend.verify_email.heading")}
subtitle={
<Trans
i18nKey="frontend.verify_email.enter_code_prompt"
values={{ email: userEmailAuthentication.email }}
components={{ email: <span className="text-primary" /> }}
/>
}
/>
<Form.Root onSubmit={onFormSubmit}>
{emailSent && (
<Alert
type="success"
title={t("frontend.verify_email.email_sent_alert.title")}
>
{t("frontend.verify_email.email_sent_alert.description")}
</Alert>
)}
{invalidCode && (
<Alert
type="critical"
title={t("frontend.verify_email.invalid_code_alert.title")}
>
{t("frontend.verify_email.invalid_code_alert.description")}
</Alert>
)}
<Form.Field
name="code"
serverInvalid={invalidCode}
className="self-center mb-4"
>
<Form.Label>{t("frontend.verify_email.code_field_label")}</Form.Label>
<Form.MFAControl ref={fieldRef} />
{invalidCode && (
<Form.ErrorMessage>
{t("frontend.verify_email.code_field_error")}
</Form.ErrorMessage>
)}
<Form.ErrorMessage match="patternMismatch">
{t("frontend.verify_email.code_field_wrong_shape")}
</Form.ErrorMessage>
</Form.Field>
<Form.Submit type="submit" disabled={verifyEmail.isPending}>
{verifyEmail.isPending && <LoadingSpinner inline />}
{t("action.continue")}
</Form.Submit>
<Button
type="button"
kind="secondary"
disabled={resendEmailAuthenticationCode.isPending}
onClick={onResendClick}
>
{resendEmailAuthenticationCode.isPending && <LoadingSpinner inline />}
{t("frontend.verify_email.resend_code")}
</Button>
<ButtonLink as="a" Icon={IconArrowLeft} kind="tertiary" to="/">
{t("action.back")}
</ButtonLink>
</Form.Root>
</Layout>
);
}

View File

@@ -5,14 +5,16 @@
// Please see LICENSE in the repository root for full details.
import { queryOptions } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, notFound, redirect } from "@tanstack/react-router";
import { graphql } from "../gql";
import { graphqlRequest } from "../graphql";
const QUERY = graphql(/* GraphQL */ `
query VerifyEmail($id: ID!) {
userEmail(id: $id) {
...UserEmail_verifyEmail
userEmailAuthentication(id: $id) {
id
email
completedAt
}
}
`);
@@ -25,6 +27,14 @@ export const query = (id: string) =>
});
export const Route = createFileRoute("/emails/$id/verify")({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(query(params.id)),
async loader({ context, params }): Promise<void> {
const data = await context.queryClient.ensureQueryData(query(params.id));
if (!data.userEmailAuthentication) {
throw notFound();
}
if (data.userEmailAuthentication.completedAt) {
throw redirect({ to: "/" });
}
},
});

View File

@@ -45,7 +45,6 @@ const userProfileHandler = ({
data: {
viewer: {
__typename: "User",
id: "user-id",
emails: {
totalCount: emailTotalCount,
},

View File

@@ -14,6 +14,7 @@ module.exports = {
theme: {
colors: {
white: "#FFFFFF",
primary: "var(--cpd-color-text-primary)",
secondary: "var(--cpd-color-text-secondary)",
critical: "var(--cpd-color-text-critical-primary)",
alert: "#FF5B55",

View File

@@ -93,7 +93,6 @@ export const handlers = [
data: {
viewer: {
__typename: "User",
id: "user-id",
emails: {
totalCount: 1,
},