1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

frontend: move the email verification form to a dedicated page

This commit is contained in:
Quentin Gliech
2023-08-04 19:30:38 +02:00
parent f001463585
commit 6002a6b929
13 changed files with 1042 additions and 635 deletions

View File

@@ -28,6 +28,7 @@ type HomeRoute = { type: "home" };
type AccountRoute = { type: "account" }; type AccountRoute = { type: "account" };
type OAuth2ClientRoute = { type: "client"; id: string }; type OAuth2ClientRoute = { type: "client"; id: string };
type BrowserSessionRoute = { type: "session"; id: string }; type BrowserSessionRoute = { type: "session"; id: string };
type VerifyEmailRoute = { type: "verify-email"; id: string };
type UnknownRoute = { type: "unknown"; segments: string[] }; type UnknownRoute = { type: "unknown"; segments: string[] };
export type Route = export type Route =
@@ -35,6 +36,7 @@ export type Route =
| AccountRoute | AccountRoute
| OAuth2ClientRoute | OAuth2ClientRoute
| BrowserSessionRoute | BrowserSessionRoute
| VerifyEmailRoute
| UnknownRoute; | UnknownRoute;
const routeToSegments = (route: Route): string[] => { const routeToSegments = (route: Route): string[] => {
@@ -47,6 +49,8 @@ const routeToSegments = (route: Route): string[] => {
return ["client", route.id]; return ["client", route.id];
case "session": case "session":
return ["session", route.id]; return ["session", route.id];
case "verify-email":
return ["verify-email", route.id];
case "unknown": case "unknown":
return route.segments; return route.segments;
} }
@@ -69,6 +73,10 @@ const segmentsToRoute = (segments: string[]): Route => {
return { type: "session", id: segments[1] }; return { type: "session", id: segments[1] };
} }
if (segments.length === 2 && segments[0] === "verify-email") {
return { type: "verify-email", id: segments[1] };
}
return { type: "unknown", segments }; return { type: "unknown", segments };
}; };
@@ -112,6 +120,7 @@ const Home = lazy(() => import("./pages/Home"));
const Account = lazy(() => import("./pages/Account")); const Account = lazy(() => import("./pages/Account"));
const OAuth2Client = lazy(() => import("./pages/OAuth2Client")); const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
const BrowserSession = lazy(() => import("./pages/BrowserSession")); const BrowserSession = lazy(() => import("./pages/BrowserSession"));
const VerifyEmail = lazy(() => import("./pages/VerifyEmail"));
const InnerRouter: React.FC = () => { const InnerRouter: React.FC = () => {
const route = useAtomValue(routeAtom); const route = useAtomValue(routeAtom);
@@ -125,6 +134,8 @@ const InnerRouter: React.FC = () => {
return <OAuth2Client id={route.id} />; return <OAuth2Client id={route.id} />;
case "session": case "session":
return <BrowserSession id={route.id} />; return <BrowserSession id={route.id} />;
case "verify-email":
return <VerifyEmail id={route.id} />;
case "unknown": case "unknown":
return <>Unknown route {JSON.stringify(route.segments)}</>; return <>Unknown route {JSON.stringify(route.segments)}</>;
} }
@@ -154,14 +165,10 @@ export const Link: React.FC<
<a <a
href={path} href={path}
onClick={(e: React.MouseEvent): void => { onClick={(e: React.MouseEvent): void => {
// Local links should be handled by the internal routers e.preventDefault();
// external links do not require a transition startTransition(() => {
if (!path.startsWith("http")) { setRoute(route);
e.preventDefault(); });
startTransition(() => {
setRoute(route);
});
}
}} }}
{...props} {...props}
> >

View File

@@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Control, Field, Root, Submit } from "@vector-im/compound-web"; import { Control, Field, Root, Submit } from "@vector-im/compound-web";
import { atom, useAtom } from "jotai"; import { useAtom } from "jotai";
import { atomWithMutation } from "jotai-urql"; import { atomWithMutation } from "jotai-urql";
import { useRef, useTransition } from "react"; import { useRef, useTransition } from "react";
@@ -35,15 +35,10 @@ const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
const addUserEmailAtom = atomWithMutation(ADD_EMAIL_MUTATION); const addUserEmailAtom = atomWithMutation(ADD_EMAIL_MUTATION);
export const latestAddedEmailAtom = atom(async (get) => { const AddEmailForm: React.FC<{
const result = await get(addUserEmailAtom); userId: string;
return result.data?.addEmail.email?.id ?? null; onAdd?: (id: string) => void;
}); }> = ({ userId, onAdd }) => {
const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
userId,
onAdd,
}) => {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const [addEmailResult, addEmail] = useAtom(addUserEmailAtom); const [addEmailResult, addEmail] = useAtom(addUserEmailAtom);
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
@@ -60,8 +55,12 @@ const AddEmailForm: React.FC<{ userId: string; onAdd?: () => void }> = ({
return; return;
} }
if (!result.data?.addEmail.email?.id) {
throw new Error("Unexpected response from server");
}
// Call the onAdd callback if provided // Call the onAdd callback if provided
onAdd?.(); onAdd?.(result.data?.addEmail.email?.id);
// Reset the form // Reset the form
formRef.current?.reset(); formRef.current?.reset();

View File

@@ -1,299 +0,0 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Button,
Control,
Field,
Label,
Message,
Root as Form,
Submit,
} from "@vector-im/compound-web";
import { atom, useAtom, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useRef, useTransition } from "react";
import { FragmentType, graphql, useFragment } from "../gql";
import Block from "./Block";
import DateTime from "./DateTime";
import Typography from "./Typography";
// 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.
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_email on UserEmail {
id
email
createdAt
confirmedAt
}
`);
const VERIFY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation VerifyEmail($id: ID!, $code: String!) {
verifyEmail(input: { userEmailId: $id, code: $code }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
const RESEND_VERIFICATION_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation ResendVerificationEmail($id: ID!) {
sendVerificationEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation RemoveEmail($id: ID!) {
removeEmail(input: { userEmailId: $id }) {
status
user {
id
}
}
}
`);
const SET_PRIMARY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation SetPrimaryEmail($id: ID!) {
setPrimaryEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
}
}
`);
const verifyEmailFamily = atomFamily((id: string) => {
const verifyEmail = atomWithMutation(VERIFY_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const verifyEmailAtom = atom(
(get) => get(verifyEmail),
(get, set, code: string) => set(verifyEmail, { id, code }),
);
return verifyEmailAtom;
});
const resendVerificationEmailFamily = atomFamily((id: string) => {
const resendVerificationEmail = atomWithMutation(
RESEND_VERIFICATION_EMAIL_MUTATION,
);
// A proxy atom which pre-sets the id variable in the mutation
const resendVerificationEmailAtom = atom(
(get) => get(resendVerificationEmail),
(get, set) => set(resendVerificationEmail, { id }),
);
return resendVerificationEmailAtom;
});
const removeEmailFamily = atomFamily((id: string) => {
const removeEmail = atomWithMutation(REMOVE_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const removeEmailAtom = atom(
(get) => get(removeEmail),
(get, set) => set(removeEmail, { id }),
);
return removeEmailAtom;
});
const setPrimaryEmailFamily = atomFamily((id: string) => {
const setPrimaryEmail = atomWithMutation(SET_PRIMARY_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const setPrimaryEmailAtom = atom(
(get) => get(setPrimaryEmail),
(get, set) => set(setPrimaryEmail, { id }),
);
return setPrimaryEmailAtom;
});
const UserEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
onRemove?: () => void;
onSetPrimary?: () => void;
isPrimary?: boolean;
highlight?: boolean;
}> = ({ email, isPrimary, highlight, onSetPrimary, onRemove }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(FRAGMENT, email);
const [verifyEmailResult, verifyEmail] = useAtom(verifyEmailFamily(data.id));
const [resendVerificationEmailResult, resendVerificationEmail] = useAtom(
resendVerificationEmailFamily(data.id),
);
const setPrimaryEmail = useSetAtom(setPrimaryEmailFamily(data.id));
const removeEmail = useSetAtom(removeEmailFamily(data.id));
const fieldRef = useRef<HTMLInputElement>(null);
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const code = formData.get("code") as string;
startTransition(() => {
verifyEmail(code).then((result) => {
// Clear the form
e.currentTarget?.reset();
if (result.data?.verifyEmail.status === "VERIFIED") {
// Call the onSetPrimary callback if provided
// XXX: do we need a dedicated onVerify callback?
onSetPrimary?.();
}
});
});
};
const onResendClick = (): void => {
startTransition(() => {
resendVerificationEmail().then(() => {
fieldRef.current?.focus();
});
});
};
const onRemoveClick = (): void => {
startTransition(() => {
removeEmail().then(() => {
// Call the onRemove callback if provided
onRemove?.();
});
});
};
const onSetPrimaryClick = (): void => {
startTransition(() => {
setPrimaryEmail().then(() => {
// Call the onSetPrimary callback if provided
onSetPrimary?.();
});
});
};
const emailSent =
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
const invalidCode =
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
return (
<Block
highlight={highlight}
className="grid grid-col-1 gap-2 pb-4 border-b-2 border-b-grey-200"
>
{isPrimary && (
<Typography variant="body" bold>
Primary
</Typography>
)}
<Typography variant="caption" bold className="flex-1">
{data.email}
</Typography>
{data.confirmedAt ? (
<Typography variant="micro">
Verified <DateTime datetime={data.confirmedAt} />
</Typography>
) : (
<Form onSubmit={onFormSubmit} className="grid grid-cols-2 gap-2">
<Field name="code" className="col-span-2">
<Label>Code</Label>
<Control
ref={fieldRef}
placeholder="xxxxxx"
type="text"
inputMode="numeric"
/>
</Field>
{invalidCode && (
<Message className="col-span-2 text-alert font-bold">
Invalid code
</Message>
)}
<Submit size="sm" type="submit" disabled={pending}>
Submit
</Submit>
<Button
size="sm"
kind="secondary"
disabled={pending || emailSent}
onClick={onResendClick}
>
{emailSent ? "Sent!" : "Resend"}
</Button>
</Form>
)}
{!isPrimary && (
<div className="flex justify-between items-center">
{/* The primary email can only be set if the email was verified */}
{data.confirmedAt ? (
<Button size="sm" disabled={pending} onClick={onSetPrimaryClick}>
Set primary
</Button>
) : (
<div />
)}
<Button
kind="destructive"
size="sm"
disabled={pending}
onClick={onRemoveClick}
>
Remove
</Button>
</div>
)}
</Block>
);
};
export default UserEmail;

View File

@@ -0,0 +1,81 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.user-email {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--cpd-space-2x);
}
.user-email-line {
display: flex;
align-self: stretch;
gap: var(--cpd-space-2x);
}
.user-email-field {
border: 1px solid var(--cpd-color-border-interactive-primary);
background: var(--cpd-color-bg-canvas-default);
border-radius: 0.5rem;
padding: var(--cpd-space-3x) var(--cpd-space-4x);
box-sizing: border-box;
flex: 1;
}
.user-email-delete {
box-sizing: border-box;
height: var(--cpd-space-12x);
width: var(--cpd-space-12x);
border-radius: 50%;
color: var(--cpd-color-icon-critical-primary);
padding: var(--cpd-space-2x);
}
.user-email-delete:hover:not([disabled]) {
background-color: var(--cpd-color-red-300);
}
.user-email-delete[disabled] {
color: var(--cpd-color-icon-disabled);
}
.user-email-delete-icon {
display: block;
height: var(--cpd-space-8x);
width: var(--cpd-space-8x);
}
.user-email-unverified {
color: var(--cpd-color-text-critical-primary);
}
.link {
display: inline-block;
text-decoration: underline;
color: var(--cpd-color-text-primary);
font-weight: var(--cpd-font-weight-medium);
border-radius: var(--cpd-radius-pill-effect);
padding-inline: 0.25rem;
}
.link:hover {
background: var(--cpd-color-gray-300);
}
.link:active {
color: var(--cpd-color-text-on-solid-primary);
}

View File

@@ -0,0 +1,164 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import IconDelete from "@vector-im/compound-design-tokens/icons/delete.svg";
import { Body } from "@vector-im/compound-web";
import { atom, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useTransition } from "react";
import { Link } from "../../Router";
import { FragmentType, graphql, useFragment } from "../../gql";
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.
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_email on UserEmail {
id
email
confirmedAt
}
`);
const REMOVE_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation RemoveEmail($id: ID!) {
removeEmail(input: { userEmailId: $id }) {
status
user {
id
}
}
}
`);
const SET_PRIMARY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation SetPrimaryEmail($id: ID!) {
setPrimaryEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
}
}
`);
const removeEmailFamily = atomFamily((id: string) => {
const removeEmail = atomWithMutation(REMOVE_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const removeEmailAtom = atom(
(get) => get(removeEmail),
(_get, set) => set(removeEmail, { id }),
);
return removeEmailAtom;
});
const setPrimaryEmailFamily = atomFamily((id: string) => {
const setPrimaryEmail = atomWithMutation(SET_PRIMARY_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const setPrimaryEmailAtom = atom(
(get) => get(setPrimaryEmail),
(_get, set) => set(setPrimaryEmail, { id }),
);
return setPrimaryEmailAtom;
});
const DeleteButton: React.FC<{ disabled?: boolean; onClick?: () => void }> = ({
disabled,
onClick,
}) => (
<button
disabled={disabled}
onClick={onClick}
className={styles.userEmailDelete}
title="Remove email address"
>
<IconDelete className={styles.userEmailDeleteIcon} />
</button>
);
const UserEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
onRemove?: () => void;
onSetPrimary?: () => void;
isPrimary?: boolean;
highlight?: boolean;
}> = ({ email, isPrimary, highlight, onSetPrimary, onRemove }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(FRAGMENT, email);
const setPrimaryEmail = useSetAtom(setPrimaryEmailFamily(data.id));
const removeEmail = useSetAtom(removeEmailFamily(data.id));
const onRemoveClick = (): void => {
startTransition(() => {
removeEmail().then(() => {
// Call the onRemove callback if provided
onRemove?.();
});
});
};
const onSetPrimaryClick = (): void => {
startTransition(() => {
setPrimaryEmail().then(() => {
// Call the onSetPrimary callback if provided
onSetPrimary?.();
});
});
};
return (
<div className={styles.userEmail}>
{isPrimary ? <Body>Primary email</Body> : <Body>Email</Body>}
<div className={styles.userEmailLine}>
<div className={styles.userEmailField}>{data.email}</div>
<DeleteButton disabled={isPrimary || pending} onClick={onRemoveClick} />
</div>
{data.confirmedAt && !isPrimary && (
<button
className={styles.link}
disabled={pending}
onClick={onSetPrimaryClick}
>
Make primary
</button>
)}
{!data.confirmedAt && (
<div>
<span className={styles.userEmailUnverified}>Unverified</span> |{" "}
<Link
className={styles.link}
route={{ type: "verify-email", id: data.id }}
>
Retry verification
</Link>
</div>
)}
</div>
);
};
export default UserEmail;

View File

@@ -0,0 +1,15 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export { default } from "./UserEmail";

View File

@@ -17,17 +17,17 @@ import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql"; import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react"; import { useTransition } from "react";
import { routeAtom } from "../Router";
import { graphql } from "../gql"; import { graphql } from "../gql";
import { PageInfo } from "../gql/graphql"; import { PageInfo } from "../gql/graphql";
import { import {
atomForCurrentPagination, atomForCurrentPagination,
atomWithPagination, atomWithPagination,
FIRST_PAGE, FIRST_PAGE,
LAST_PAGE,
Pagination, Pagination,
} from "../pagination"; } from "../pagination";
import AddEmailForm, { latestAddedEmailAtom } from "./AddEmailForm"; import AddEmailForm from "./AddEmailForm";
import BlockList from "./BlockList"; import BlockList from "./BlockList";
import PaginationControls from "./PaginationControls"; import PaginationControls from "./PaginationControls";
import UserEmail from "./UserEmail"; import UserEmail from "./UserEmail";
@@ -129,12 +129,11 @@ const UserEmailList: React.FC<{
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const [result, refreshList] = useAtom(emailPageResultFamily(userId)); const [result, refreshList] = useAtom(emailPageResultFamily(userId));
const setPagination = useSetAtom(currentPaginationAtom); const setPagination = useSetAtom(currentPaginationAtom);
const setRoute = useSetAtom(routeAtom);
const [prevPage, nextPage] = useAtomValue(paginationFamily(userId)); const [prevPage, nextPage] = useAtomValue(paginationFamily(userId));
const [primaryEmailId, refreshPrimaryEmailId] = useAtom( const [primaryEmailId, refreshPrimaryEmailId] = useAtom(
primaryEmailIdFamily(userId), primaryEmailIdFamily(userId),
); );
// XXX: we may not want to directly use that atom here, but rather have a local state
const latestAddedEmail = useAtomValue(latestAddedEmailAtom);
const paginate = (pagination: Pagination): void => { const paginate = (pagination: Pagination): void => {
startTransition(() => { startTransition(() => {
@@ -150,12 +149,9 @@ const UserEmailList: React.FC<{
}); });
}; };
// When adding an email, we want to refresh the list and go to the last page // When adding an email, we want to go to the email verification form
const onAdd = (): void => { const onAdd = (id: string): void => {
startTransition(() => { setRoute({ type: "verify-email", id });
setPagination(LAST_PAGE);
refreshList();
});
}; };
return ( return (
@@ -173,7 +169,6 @@ const UserEmailList: React.FC<{
isPrimary={primaryEmailId === edge.node.id} isPrimary={primaryEmailId === edge.node.id}
onSetPrimary={refreshPrimaryEmailId} onSetPrimary={refreshPrimaryEmailId}
onRemove={onRemove} onRemove={onRemove}
highlight={latestAddedEmail === edge.node.id}
/> />
))} ))}
<AddEmailForm userId={userId} onAdd={onAdd} /> <AddEmailForm userId={userId} onAdd={onAdd} />

View File

@@ -0,0 +1,57 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.header {
margin: var(--cpd-space-8x) 0;
text-align: center;
}
.title {
font: var(--cpd-font-heading-xl-semibold);
line-height: var(--cpd-font-line-height-regular);
color: var(--cpd-color-text-primary);
}
.tagline {
font: var(--cpd-font-body-lg-regular);
line-height: var(--cpd-font-line-height-regular);
color: var(--cpd-color-text-secondary);
margin-top: var(--cpd-space-2x);
}
.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: var(--cpd-space-4x) auto;
}
.email {
font: var(--cpd-font-body-lg-semibold);
}
.form {
margin: var(--cpd-space-8x) 0;
display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
}
.submit-button {
display: block;
}

View File

@@ -0,0 +1,199 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import IconSend from "@vector-im/compound-design-tokens/icons/check.svg";
import {
Button,
Control,
Field,
Label,
Submit,
Root as Form,
Alert,
} from "@vector-im/compound-web";
import { useSetAtom, atom, useAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useEffect, useRef, useTransition } from "react";
import { routeAtom } from "../../Router";
import { FragmentType, graphql, useFragment } from "../../gql";
import styles from "./VerifyEmail.module.css";
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_verifyEmail on UserEmail {
id
email
}
`);
const VERIFY_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation VerifyEmail($id: ID!, $code: String!) {
verifyEmail(input: { userEmailId: $id, code: $code }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
const RESEND_VERIFICATION_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation ResendVerificationEmail($id: ID!) {
sendVerificationEmail(input: { userEmailId: $id }) {
status
user {
id
primaryEmail {
id
}
}
email {
id
...UserEmail_email
}
}
}
`);
const verifyEmailFamily = atomFamily((id: string) => {
const verifyEmail = atomWithMutation(VERIFY_EMAIL_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
const verifyEmailAtom = atom(
(get) => get(verifyEmail),
(get, set, code: string) => set(verifyEmail, { id, code }),
);
return verifyEmailAtom;
});
const resendVerificationEmailFamily = atomFamily((id: string) => {
const resendVerificationEmail = atomWithMutation(
RESEND_VERIFICATION_EMAIL_MUTATION,
);
// A proxy atom which pre-sets the id variable in the mutation
const resendVerificationEmailAtom = atom(
(get) => get(resendVerificationEmail),
(_get, set) => set(resendVerificationEmail, { id }),
);
return resendVerificationEmailAtom;
});
const VerifyEmail: React.FC<{
email: FragmentType<typeof FRAGMENT>;
}> = ({ email }) => {
const data = useFragment(FRAGMENT, email);
const [pending, startTransition] = useTransition();
const [verifyEmailResult, verifyEmail] = useAtom(verifyEmailFamily(data.id));
const [resendVerificationEmailResult, resendVerificationEmail] = useAtom(
resendVerificationEmailFamily(data.id),
);
const setRoute = useSetAtom(routeAtom);
const fieldRef = useRef<HTMLInputElement>(null);
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const code = formData.get("code") as string;
startTransition(() => {
verifyEmail(code).then((result) => {
// Clear the form
e.currentTarget?.reset();
if (result.data?.verifyEmail.status === "VERIFIED") {
setRoute({ type: "account" });
} else {
fieldRef.current?.focus();
fieldRef.current?.select();
}
});
});
};
// Focus the field on mount
useEffect(() => {
fieldRef.current?.focus();
}, [fieldRef]);
const onResendClick = (): void => {
startTransition(() => {
resendVerificationEmail().then(() => {
fieldRef.current?.focus();
});
});
};
const emailSent =
resendVerificationEmailResult.data?.sendVerificationEmail.status === "SENT";
const invalidCode =
verifyEmailResult.data?.verifyEmail.status === "INVALID_CODE";
return (
<>
<header className={styles.header}>
<IconSend className={styles.icon} />
<h1 className={styles.title}>Verify your email</h1>
<p className={styles.tagline}>
Enter the 6-digit code sent to{" "}
<span className={styles.email}>{data.email}</span>
</p>
</header>
<Form onSubmit={onFormSubmit} className={styles.form}>
{invalidCode && <Alert type="critical" title="Invalid code" />}
<Field name="code" serverInvalid={invalidCode}>
<Label>6-digit code</Label>
<Control
ref={fieldRef}
placeholder="xxxxxx"
type="text"
inputMode="numeric"
/>
</Field>
<Submit
type="submit"
disabled={pending}
className={styles.submitButton}
>
Continue
</Submit>
<Button
kind="tertiary"
disabled={pending || emailSent}
onClick={onResendClick}
>
{emailSent ? "Sent!" : "Resend email"}
</Button>
</Form>
</>
);
};
export default VerifyEmail;

View File

@@ -0,0 +1,15 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export { default } from "./VerifyEmail";

View File

@@ -39,12 +39,8 @@ const documents = {
types.EndOAuth2SessionDocument, types.EndOAuth2SessionDocument,
"\n query OAuth2SessionListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": "\n query OAuth2SessionListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.OAuth2SessionListQueryDocument, types.OAuth2SessionListQueryDocument,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n": "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n":
types.UserEmail_EmailFragmentDoc, types.UserEmail_EmailFragmentDoc,
"\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.VerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.ResendVerificationEmailDocument,
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n":
types.RemoveEmailDocument, types.RemoveEmailDocument,
"\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n": "\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n":
@@ -55,10 +51,18 @@ const documents = {
types.UserPrimaryEmailDocument, types.UserPrimaryEmailDocument,
"\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n": "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n":
types.UserGreetingDocument, types.UserGreetingDocument,
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n":
types.UserEmail_VerifyEmailFragmentDoc,
"\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.VerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.ResendVerificationEmailDocument,
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n": "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
types.BrowserSessionQueryDocument, types.BrowserSessionQueryDocument,
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n": "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n":
types.OAuth2ClientQueryDocument, types.OAuth2ClientQueryDocument,
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
types.VerifyEmailQueryDocument,
}; };
/** /**
@@ -157,20 +161,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n", source: "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n",
): (typeof documents)["\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n"]; ): (typeof documents)["\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\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 mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\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 mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -201,6 +193,24 @@ export function graphql(
export function graphql( export function graphql(
source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n", source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n",
): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n"]; ): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\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_verifyEmail on UserEmail {\n id\n email\n }\n",
): (typeof documents)["\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\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 mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\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 mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n",
): (typeof documents)["\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -213,6 +223,12 @@ export function graphql(
export function graphql( export function graphql(
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n", source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n",
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n"]; ): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\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 query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n",
): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"];
export function graphql(source: string) { export function graphql(source: string) {
return (documents as any)[source] ?? {}; return (documents as any)[source] ?? {};

View File

@@ -1128,55 +1128,9 @@ export type UserEmail_EmailFragment = {
__typename?: "UserEmail"; __typename?: "UserEmail";
id: string; id: string;
email: string; email: string;
createdAt: any;
confirmedAt?: any | null; confirmedAt?: any | null;
} & { " $fragmentName"?: "UserEmail_EmailFragment" }; } & { " $fragmentName"?: "UserEmail_EmailFragment" };
export type VerifyEmailMutationVariables = Exact<{
id: Scalars["ID"]["input"];
code: Scalars["String"]["input"];
}>;
export type VerifyEmailMutation = {
__typename?: "Mutation";
verifyEmail: {
__typename?: "VerifyEmailPayload";
status: VerifyEmailStatus;
user?: {
__typename?: "User";
id: string;
primaryEmail?: { __typename?: "UserEmail"; id: string } | null;
} | null;
email?:
| ({ __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: {
UserEmail_EmailFragment: UserEmail_EmailFragment;
};
})
| null;
};
};
export type ResendVerificationEmailMutationVariables = Exact<{
id: Scalars["ID"]["input"];
}>;
export type ResendVerificationEmailMutation = {
__typename?: "Mutation";
sendVerificationEmail: {
__typename?: "SendVerificationEmailPayload";
status: SendVerificationEmailStatus;
user: {
__typename?: "User";
id: string;
primaryEmail?: { __typename?: "UserEmail"; id: string } | null;
};
email: { __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
};
};
};
export type RemoveEmailMutationVariables = Exact<{ export type RemoveEmailMutationVariables = Exact<{
id: Scalars["ID"]["input"]; id: Scalars["ID"]["input"];
}>; }>;
@@ -1274,6 +1228,57 @@ export type UserGreetingQuery = {
} | null; } | null;
}; };
export type UserEmail_VerifyEmailFragment = {
__typename?: "UserEmail";
id: string;
email: string;
} & { " $fragmentName"?: "UserEmail_VerifyEmailFragment" };
export type VerifyEmailMutationVariables = Exact<{
id: Scalars["ID"]["input"];
code: Scalars["String"]["input"];
}>;
export type VerifyEmailMutation = {
__typename?: "Mutation";
verifyEmail: {
__typename?: "VerifyEmailPayload";
status: VerifyEmailStatus;
user?: {
__typename?: "User";
id: string;
primaryEmail?: { __typename?: "UserEmail"; id: string } | null;
} | null;
email?:
| ({ __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: {
UserEmail_EmailFragment: UserEmail_EmailFragment;
};
})
| null;
};
};
export type ResendVerificationEmailMutationVariables = Exact<{
id: Scalars["ID"]["input"];
}>;
export type ResendVerificationEmailMutation = {
__typename?: "Mutation";
sendVerificationEmail: {
__typename?: "SendVerificationEmailPayload";
status: SendVerificationEmailStatus;
user: {
__typename?: "User";
id: string;
primaryEmail?: { __typename?: "UserEmail"; id: string } | null;
};
email: { __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
};
};
};
export type BrowserSessionQueryQueryVariables = Exact<{ export type BrowserSessionQueryQueryVariables = Exact<{
id: Scalars["ID"]["input"]; id: Scalars["ID"]["input"];
}>; }>;
@@ -1311,6 +1316,21 @@ export type OAuth2ClientQueryQuery = {
} | null; } | null;
}; };
export type VerifyEmailQueryQueryVariables = Exact<{
id: Scalars["ID"]["input"];
}>;
export type VerifyEmailQueryQuery = {
__typename?: "Query";
userEmail?:
| ({ __typename?: "UserEmail" } & {
" $fragmentRefs"?: {
UserEmail_VerifyEmailFragment: UserEmail_VerifyEmailFragment;
};
})
| null;
};
export const BrowserSession_SessionFragmentDoc = { export const BrowserSession_SessionFragmentDoc = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -1463,13 +1483,32 @@ export const UserEmail_EmailFragmentDoc = {
selections: [ selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } }, { kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } }, { kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
], ],
}, },
}, },
], ],
} as unknown as DocumentNode<UserEmail_EmailFragment, unknown>; } as unknown as DocumentNode<UserEmail_EmailFragment, unknown>;
export const UserEmail_VerifyEmailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_verifyEmail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
],
},
},
],
} as unknown as DocumentNode<UserEmail_VerifyEmailFragment, unknown>;
export const CurrentViewerQueryDocument = { export const CurrentViewerQueryDocument = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -1681,7 +1720,6 @@ export const AddEmailDocument = {
selections: [ selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } }, { kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } }, { kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
], ],
}, },
@@ -2615,244 +2653,6 @@ export const OAuth2SessionListQueryDocument = {
OAuth2SessionListQueryQuery, OAuth2SessionListQueryQuery,
OAuth2SessionListQueryQueryVariables OAuth2SessionListQueryQueryVariables
>; >;
export const VerifyEmailDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "VerifyEmail" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
},
},
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "code" } },
type: {
kind: "NonNullType",
type: {
kind: "NamedType",
name: { kind: "Name", value: "String" },
},
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "verifyEmail" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: {
kind: "ObjectValue",
fields: [
{
kind: "ObjectField",
name: { kind: "Name", value: "userEmailId" },
value: {
kind: "Variable",
name: { kind: "Name", value: "id" },
},
},
{
kind: "ObjectField",
name: { kind: "Name", value: "code" },
value: {
kind: "Variable",
name: { kind: "Name", value: "code" },
},
},
],
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "status" } },
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "id" },
},
],
},
},
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "email" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
],
} as unknown as DocumentNode<VerifyEmailMutation, VerifyEmailMutationVariables>;
export const ResendVerificationEmailDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "ResendVerificationEmail" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "sendVerificationEmail" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: {
kind: "ObjectValue",
fields: [
{
kind: "ObjectField",
name: { kind: "Name", value: "userEmailId" },
value: {
kind: "Variable",
name: { kind: "Name", value: "id" },
},
},
],
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "status" } },
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "id" },
},
],
},
},
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "email" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
],
} as unknown as DocumentNode<
ResendVerificationEmailMutation,
ResendVerificationEmailMutationVariables
>;
export const RemoveEmailDocument = { export const RemoveEmailDocument = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -3189,7 +2989,6 @@ export const UserEmailListQueryDocument = {
selections: [ selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } }, { kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "createdAt" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } }, { kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
], ],
}, },
@@ -3323,6 +3122,242 @@ export const UserGreetingDocument = {
}, },
], ],
} as unknown as DocumentNode<UserGreetingQuery, UserGreetingQueryVariables>; } as unknown as DocumentNode<UserGreetingQuery, UserGreetingQueryVariables>;
export const VerifyEmailDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "VerifyEmail" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
},
},
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "code" } },
type: {
kind: "NonNullType",
type: {
kind: "NamedType",
name: { kind: "Name", value: "String" },
},
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "verifyEmail" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: {
kind: "ObjectValue",
fields: [
{
kind: "ObjectField",
name: { kind: "Name", value: "userEmailId" },
value: {
kind: "Variable",
name: { kind: "Name", value: "id" },
},
},
{
kind: "ObjectField",
name: { kind: "Name", value: "code" },
value: {
kind: "Variable",
name: { kind: "Name", value: "code" },
},
},
],
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "status" } },
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "id" },
},
],
},
},
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "email" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
],
} as unknown as DocumentNode<VerifyEmailMutation, VerifyEmailMutationVariables>;
export const ResendVerificationEmailDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "mutation",
name: { kind: "Name", value: "ResendVerificationEmail" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "sendVerificationEmail" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "input" },
value: {
kind: "ObjectValue",
fields: [
{
kind: "ObjectField",
name: { kind: "Name", value: "userEmailId" },
value: {
kind: "Variable",
name: { kind: "Name", value: "id" },
},
},
],
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "status" } },
{
kind: "Field",
name: { kind: "Name", value: "user" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "id" },
},
],
},
},
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "email" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
],
} as unknown as DocumentNode<
ResendVerificationEmailMutation,
ResendVerificationEmailMutationVariables
>;
export const BrowserSessionQueryDocument = { export const BrowserSessionQueryDocument = {
kind: "Document", kind: "Document",
definitions: [ definitions: [
@@ -3457,3 +3492,69 @@ export const OAuth2ClientQueryDocument = {
OAuth2ClientQueryQuery, OAuth2ClientQueryQuery,
OAuth2ClientQueryQueryVariables OAuth2ClientQueryQueryVariables
>; >;
export const VerifyEmailQueryDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "query",
name: { kind: "Name", value: "VerifyEmailQuery" },
variableDefinitions: [
{
kind: "VariableDefinition",
variable: { kind: "Variable", name: { kind: "Name", value: "id" } },
type: {
kind: "NonNullType",
type: { kind: "NamedType", name: { kind: "Name", value: "ID" } },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "userEmail" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "id" },
value: {
kind: "Variable",
name: { kind: "Name", value: "id" },
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_verifyEmail" },
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_verifyEmail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
],
},
},
],
} as unknown as DocumentNode<
VerifyEmailQueryQuery,
VerifyEmailQueryQueryVariables
>;

View File

@@ -0,0 +1,57 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { useAtomValue } from "jotai";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { mapQueryAtom } from "../atoms";
import GraphQLError from "../components/GraphQLError";
import VerifyEmailComponent from "../components/VerifyEmail";
import { graphql } from "../gql";
import { isErr, unwrapErr, unwrapOk } from "../result";
const QUERY = graphql(/* GraphQL */ `
query VerifyEmailQuery($id: ID!) {
userEmail(id: $id) {
...UserEmail_verifyEmail
}
}
`);
const verifyEmailFamily = atomFamily((id: string) => {
const verifyEmailQueryAtom = atomWithQuery({
query: QUERY,
getVariables: () => ({ id }),
});
const verifyEmailAtom = mapQueryAtom(
verifyEmailQueryAtom,
(data) => data?.userEmail,
);
return verifyEmailAtom;
});
const VerifyEmail: React.FC<{ id: string }> = ({ id }) => {
const result = useAtomValue(verifyEmailFamily(id));
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const email = unwrapOk(result);
if (email == null) return <>Unknown email</>;
return <VerifyEmailComponent email={email} />;
};
export default VerifyEmail;