diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 4d30b7e28..15ee71401 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -186,7 +186,6 @@ } }, "reset_cross_signing": { - "button": "Reset identity", "cancelled": { "description_1": "You can close this window and go back to the app to continue.", "description_2": "If you're signed out everywhere and don't remember your recovery code, you'll still need to reset your identity.", @@ -194,15 +193,17 @@ }, "description": "If you're not signed in to any other devices and you've lost your recovery key, then you'll need to reset your identity to continue using the app.", "effect_list": { - "negative_1": "You will lose your existing message history", - "negative_2": "You will need to verify all your existing devices and contacts again", + "neutral_1": "You will lose any message history that's stored only on the server", + "neutral_2": "You will need to verify all your existing devices and contacts again", "positive_1": "Your account details, contacts, preferences, and chat list will be kept" }, "failure": { "description": "This might be a temporary problem, so please try again later. If the problem persists, please contact your server administrator.", "heading": "Failed to allow crypto identity reset" }, + "finish_reset": "Finish reset", "heading": "Reset your identity in case you can't confirm another way", + "start_reset": "Start reset", "success": { "description": "The identity reset has been approved for the next {{minutes}} minutes. You can close this window and go back to the app to continue.", "heading": "Identity reset successfully. Go back to the app to finish the process." diff --git a/frontend/src/@types/i18next.d.ts b/frontend/src/@types/i18next.d.ts index 127b3670f..cf32447c2 100644 --- a/frontend/src/@types/i18next.d.ts +++ b/frontend/src/@types/i18next.d.ts @@ -5,15 +5,15 @@ // Please see LICENSE in the repository root for full details. import "i18next"; -import type frontend from "../../locales/en.json"; +import type translation from "../../locales/en.json"; declare module "i18next" { interface CustomTypeOptions { keySeparator: "."; pluralSeparator: ":"; - defaultNS: "frontend"; + defaultNS: "translation"; resources: { - frontend: typeof frontend; + translation: typeof translation; }; } } diff --git a/frontend/src/components/ButtonLink.module.css b/frontend/src/components/ButtonLink.module.css new file mode 100644 index 000000000..70300b40d --- /dev/null +++ b/frontend/src/components/ButtonLink.module.css @@ -0,0 +1,13 @@ +/* Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +/* The weird selector is to have higher specificity than compound-web's button-link */ +a.button-link[href] { + /** This is to undo the following rule in compound-web: + * https://github.com/element-hq/compound-web/blob/6ccb4b6049f3bc8e9739d9452c850ed3c7de49f9/src/components/Button/Button.module.css#L31-L34 + */ + inline-size: initial; +} diff --git a/frontend/src/components/ButtonLink.tsx b/frontend/src/components/ButtonLink.tsx index e8c575121..2a0a6b8b8 100644 --- a/frontend/src/components/ButtonLink.tsx +++ b/frontend/src/components/ButtonLink.tsx @@ -6,7 +6,9 @@ import { createLink } from "@tanstack/react-router"; import { Button } from "@vector-im/compound-web"; +import cx from "classnames"; import { type PropsWithChildren, forwardRef } from "react"; +import styles from "./ButtonLink.module.css"; type Props = { kind?: "primary" | "secondary" | "tertiary"; @@ -14,14 +16,21 @@ type Props = { Icon?: React.ComponentType>; destructive?: boolean; disabled?: boolean; + className?: string; } & React.AnchorHTMLAttributes; export const ButtonLink = createLink( forwardRef>( - ({ children, ...props }, ref) => { + ({ children, className, ...props }, ref) => { const disabled = !!props.disabled || !!props["aria-disabled"] || false; return ( - ); diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap index 08186e831..6242b69a3 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap @@ -133,7 +133,7 @@ exports[` > renders a compatability session details 1`] = ` />

See your profile info and contact details

@@ -154,7 +154,7 @@ exports[` > renders a compatability session details 1`] = ` />

View your existing messages and data

@@ -177,7 +177,7 @@ exports[` > renders a compatability session details 1`] = ` />

Send new messages on your behalf

@@ -393,7 +393,7 @@ exports[` > renders a compatability session without an ssoL />

See your profile info and contact details

@@ -414,7 +414,7 @@ exports[` > renders a compatability session without an ssoL />

View your existing messages and data

@@ -437,7 +437,7 @@ exports[` > renders a compatability session without an ssoL />

Send new messages on your behalf

@@ -622,7 +622,7 @@ exports[` > renders a finished compatability session detail />

See your profile info and contact details

@@ -643,7 +643,7 @@ exports[` > renders a finished compatability session detail />

View your existing messages and data

@@ -666,7 +666,7 @@ exports[` > renders a finished compatability session detail />

Send new messages on your behalf

diff --git a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap index 5d3f0c9c5..bbfbadc47 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap @@ -147,7 +147,7 @@ exports[` > renders a finished session details 1`] = ` />

See your profile info and contact details

@@ -168,7 +168,7 @@ exports[` > renders a finished session details 1`] = ` />

View your existing messages and data

@@ -191,7 +191,7 @@ exports[` > renders a finished session details 1`] = ` />

Send new messages on your behalf

@@ -394,7 +394,7 @@ exports[` > renders session details 1`] = ` />

See your profile info and contact details

@@ -415,7 +415,7 @@ exports[` > renders session details 1`] = ` />

View your existing messages and data

@@ -438,7 +438,7 @@ exports[` > renders session details 1`] = ` />

Send new messages on your behalf

diff --git a/frontend/src/components/VisualList/VisualList.module.css b/frontend/src/components/VisualList/VisualList.module.css index ff7df0e4c..7f60c94bf 100644 --- a/frontend/src/components/VisualList/VisualList.module.css +++ b/frontend/src/components/VisualList/VisualList.module.css @@ -14,7 +14,7 @@ } .item { - background: var(--cpd-color-bg-subtle-secondary); + background: var(--cpd-color-bg-action-secondary-hovered); padding: var(--cpd-space-3x) var(--cpd-space-5x); display: flex; align-items: center; diff --git a/frontend/src/components/VisualList/VisualList.tsx b/frontend/src/components/VisualList/VisualList.tsx index 33fbf415e..7eda8818b 100644 --- a/frontend/src/components/VisualList/VisualList.tsx +++ b/frontend/src/components/VisualList/VisualList.tsx @@ -30,9 +30,7 @@ export const VisualListItem: FC<{ return (
  • - - {label} - + {label}
  • ); }; diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 6bed30957..3ff54b794 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -89,7 +89,7 @@ i18n fallbackLng: "en", keySeparator: ".", pluralSeparator: ":", - defaultNS: "frontend", + defaultNS: "translation", supportedLngs, interpolation: { escapeValue: false, // React has built-in XSS protections diff --git a/frontend/src/i18n/password_changes.ts b/frontend/src/i18n/password_changes.ts index 27a77aba0..f51a7b9b6 100644 --- a/frontend/src/i18n/password_changes.ts +++ b/frontend/src/i18n/password_changes.ts @@ -21,7 +21,7 @@ import { SetPasswordStatus } from "../gql/graphql"; * Throws an error if the status is not known. */ export function translateSetPasswordError( - t: TFunction<"frontend", undefined>, + t: TFunction, status: SetPasswordStatus | undefined, ): string | undefined { switch (status) { diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index f1d6e4762..d0bb82c00 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -15,7 +15,10 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as ResetCrossSigningImport } from './routes/reset-cross-signing' import { Route as AccountImport } from './routes/_account' +import { Route as ResetCrossSigningIndexImport } from './routes/reset-cross-signing.index' import { Route as AccountIndexImport } from './routes/_account.index' +import { Route as ResetCrossSigningSuccessImport } from './routes/reset-cross-signing.success' +import { Route as ResetCrossSigningCancelledImport } from './routes/reset-cross-signing.cancelled' import { Route as DevicesSplatImport } from './routes/devices.$' import { Route as ClientsIdImport } from './routes/clients.$id' import { Route as PasswordRecoveryIndexImport } from './routes/password.recovery.index' @@ -37,15 +40,19 @@ const ResetCrossSigningRoute = ResetCrossSigningImport.update({ id: '/reset-cross-signing', path: '/reset-cross-signing', getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/reset-cross-signing.lazy').then((d) => d.Route), -) +} as any) const AccountRoute = AccountImport.update({ id: '/_account', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/_account.lazy').then((d) => d.Route)) +const ResetCrossSigningIndexRoute = ResetCrossSigningIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => ResetCrossSigningRoute, +} as any) + const AccountIndexRoute = AccountIndexImport.update({ id: '/', path: '/', @@ -54,6 +61,20 @@ const AccountIndexRoute = AccountIndexImport.update({ import('./routes/_account.index.lazy').then((d) => d.Route), ) +const ResetCrossSigningSuccessRoute = ResetCrossSigningSuccessImport.update({ + id: '/success', + path: '/success', + getParentRoute: () => ResetCrossSigningRoute, +} as any) + +const ResetCrossSigningCancelledRoute = ResetCrossSigningCancelledImport.update( + { + id: '/cancelled', + path: '/cancelled', + getParentRoute: () => ResetCrossSigningRoute, + } as any, +) + const DevicesSplatRoute = DevicesSplatImport.update({ id: '/devices/$', path: '/devices/$', @@ -154,6 +175,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DevicesSplatImport parentRoute: typeof rootRoute } + '/reset-cross-signing/cancelled': { + id: '/reset-cross-signing/cancelled' + path: '/cancelled' + fullPath: '/reset-cross-signing/cancelled' + preLoaderRoute: typeof ResetCrossSigningCancelledImport + parentRoute: typeof ResetCrossSigningImport + } + '/reset-cross-signing/success': { + id: '/reset-cross-signing/success' + path: '/success' + fullPath: '/reset-cross-signing/success' + preLoaderRoute: typeof ResetCrossSigningSuccessImport + parentRoute: typeof ResetCrossSigningImport + } '/_account/': { id: '/_account/' path: '/' @@ -161,6 +196,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountIndexImport parentRoute: typeof AccountImport } + '/reset-cross-signing/': { + id: '/reset-cross-signing/' + path: '/' + fullPath: '/reset-cross-signing/' + preLoaderRoute: typeof ResetCrossSigningIndexImport + parentRoute: typeof ResetCrossSigningImport + } '/_account/sessions/$id': { id: '/_account/sessions/$id' path: '/sessions/$id' @@ -232,12 +274,30 @@ const AccountRouteChildren: AccountRouteChildren = { const AccountRouteWithChildren = AccountRoute._addFileChildren(AccountRouteChildren) +interface ResetCrossSigningRouteChildren { + ResetCrossSigningCancelledRoute: typeof ResetCrossSigningCancelledRoute + ResetCrossSigningSuccessRoute: typeof ResetCrossSigningSuccessRoute + ResetCrossSigningIndexRoute: typeof ResetCrossSigningIndexRoute +} + +const ResetCrossSigningRouteChildren: ResetCrossSigningRouteChildren = { + ResetCrossSigningCancelledRoute: ResetCrossSigningCancelledRoute, + ResetCrossSigningSuccessRoute: ResetCrossSigningSuccessRoute, + ResetCrossSigningIndexRoute: ResetCrossSigningIndexRoute, +} + +const ResetCrossSigningRouteWithChildren = + ResetCrossSigningRoute._addFileChildren(ResetCrossSigningRouteChildren) + export interface FileRoutesByFullPath { '': typeof AccountRouteWithChildren - '/reset-cross-signing': typeof ResetCrossSigningRoute + '/reset-cross-signing': typeof ResetCrossSigningRouteWithChildren '/clients/$id': typeof ClientsIdRoute '/devices/$': typeof DevicesSplatRoute + '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute + '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute '/': typeof AccountIndexRoute + '/reset-cross-signing/': typeof ResetCrossSigningIndexRoute '/sessions/$id': typeof AccountSessionsIdRoute '/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -248,10 +308,12 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { - '/reset-cross-signing': typeof ResetCrossSigningRoute '/clients/$id': typeof ClientsIdRoute '/devices/$': typeof DevicesSplatRoute + '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute + '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute '/': typeof AccountIndexRoute + '/reset-cross-signing': typeof ResetCrossSigningIndexRoute '/sessions/$id': typeof AccountSessionsIdRoute '/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -264,10 +326,13 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRoute '/_account': typeof AccountRouteWithChildren - '/reset-cross-signing': typeof ResetCrossSigningRoute + '/reset-cross-signing': typeof ResetCrossSigningRouteWithChildren '/clients/$id': typeof ClientsIdRoute '/devices/$': typeof DevicesSplatRoute + '/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute + '/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute '/_account/': typeof AccountIndexRoute + '/reset-cross-signing/': typeof ResetCrossSigningIndexRoute '/_account/sessions/$id': typeof AccountSessionsIdRoute '/_account/sessions/browsers': typeof AccountSessionsBrowsersRoute '/emails/$id/verify': typeof EmailsIdVerifyRoute @@ -284,7 +349,10 @@ export interface FileRouteTypes { | '/reset-cross-signing' | '/clients/$id' | '/devices/$' + | '/reset-cross-signing/cancelled' + | '/reset-cross-signing/success' | '/' + | '/reset-cross-signing/' | '/sessions/$id' | '/sessions/browsers' | '/emails/$id/verify' @@ -294,10 +362,12 @@ export interface FileRouteTypes { | '/password/recovery' fileRoutesByTo: FileRoutesByTo to: - | '/reset-cross-signing' | '/clients/$id' | '/devices/$' + | '/reset-cross-signing/cancelled' + | '/reset-cross-signing/success' | '/' + | '/reset-cross-signing' | '/sessions/$id' | '/sessions/browsers' | '/emails/$id/verify' @@ -311,7 +381,10 @@ export interface FileRouteTypes { | '/reset-cross-signing' | '/clients/$id' | '/devices/$' + | '/reset-cross-signing/cancelled' + | '/reset-cross-signing/success' | '/_account/' + | '/reset-cross-signing/' | '/_account/sessions/$id' | '/_account/sessions/browsers' | '/emails/$id/verify' @@ -324,7 +397,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { AccountRoute: typeof AccountRouteWithChildren - ResetCrossSigningRoute: typeof ResetCrossSigningRoute + ResetCrossSigningRoute: typeof ResetCrossSigningRouteWithChildren ClientsIdRoute: typeof ClientsIdRoute DevicesSplatRoute: typeof DevicesSplatRoute EmailsIdVerifyRoute: typeof EmailsIdVerifyRoute @@ -335,7 +408,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { AccountRoute: AccountRouteWithChildren, - ResetCrossSigningRoute: ResetCrossSigningRoute, + ResetCrossSigningRoute: ResetCrossSigningRouteWithChildren, ClientsIdRoute: ClientsIdRoute, DevicesSplatRoute: DevicesSplatRoute, EmailsIdVerifyRoute: EmailsIdVerifyRoute, @@ -376,7 +449,12 @@ export const routeTree = rootRoute ] }, "/reset-cross-signing": { - "filePath": "reset-cross-signing.tsx" + "filePath": "reset-cross-signing.tsx", + "children": [ + "/reset-cross-signing/cancelled", + "/reset-cross-signing/success", + "/reset-cross-signing/" + ] }, "/clients/$id": { "filePath": "clients.$id.tsx" @@ -384,10 +462,22 @@ export const routeTree = rootRoute "/devices/$": { "filePath": "devices.$.tsx" }, + "/reset-cross-signing/cancelled": { + "filePath": "reset-cross-signing.cancelled.tsx", + "parent": "/reset-cross-signing" + }, + "/reset-cross-signing/success": { + "filePath": "reset-cross-signing.success.tsx", + "parent": "/reset-cross-signing" + }, "/_account/": { "filePath": "_account.index.tsx", "parent": "/_account" }, + "/reset-cross-signing/": { + "filePath": "reset-cross-signing.index.tsx", + "parent": "/reset-cross-signing" + }, "/_account/sessions/$id": { "filePath": "_account.sessions.$id.tsx", "parent": "/_account" diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx index 4ea1649ee..b716322b7 100644 --- a/frontend/src/routes/_account.index.lazy.tsx +++ b/frontend/src/routes/_account.index.lazy.tsx @@ -92,8 +92,12 @@ function Index(): React.ReactElement { {t("frontend.reset_cross_signing.description")} - - {t("frontend.reset_cross_signing.button")} + + {t("frontend.reset_cross_signing.start_reset")} diff --git a/frontend/src/routes/reset-cross-signing.cancelled.tsx b/frontend/src/routes/reset-cross-signing.cancelled.tsx new file mode 100644 index 000000000..756b8cd97 --- /dev/null +++ b/frontend/src/routes/reset-cross-signing.cancelled.tsx @@ -0,0 +1,31 @@ +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import { createFileRoute } from "@tanstack/react-router"; +import IconKeyOffSolid from "@vector-im/compound-design-tokens/assets/web/icons/key-off-solid"; +import { Text } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; +import PageHeading from "../components/PageHeading"; + +export const Route = createFileRoute("/reset-cross-signing/cancelled")({ + component: () => { + const { t } = useTranslation(); + return ( + <> + + + {t("frontend.reset_cross_signing.cancelled.description_1")} + + + {t("frontend.reset_cross_signing.cancelled.description_2")} + + + ); + }, +}); diff --git a/frontend/src/routes/reset-cross-signing.index.tsx b/frontend/src/routes/reset-cross-signing.index.tsx new file mode 100644 index 000000000..54862f479 --- /dev/null +++ b/frontend/src/routes/reset-cross-signing.index.tsx @@ -0,0 +1,150 @@ +// Copyright 2024 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 { createFileRoute, notFound } from "@tanstack/react-router"; +import IconCheck from "@vector-im/compound-design-tokens/assets/web/icons/check"; +import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info"; +import { Button, Text } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; +import { useMutation, useQuery } from "urql"; +import { ButtonLink } from "../components/ButtonLink"; +import LoadingSpinner from "../components/LoadingSpinner"; +import PageHeading from "../components/PageHeading"; +import { + VisualList, + VisualListItem, +} from "../components/VisualList/VisualList"; +import { graphql } from "../gql"; + +const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ ` + query CurrentViewerQuery { + viewer { + __typename + ... on Node { + id + } + } + } +`); + +export const Route = createFileRoute("/reset-cross-signing/")({ + async loader({ context, abortController: { signal } }) { + const viewer = await context.client.query( + CURRENT_VIEWER_QUERY, + {}, + { fetchOptions: { signal } }, + ); + if (viewer.error) throw viewer.error; + if (viewer.data?.viewer.__typename !== "User") throw notFound(); + }, + + component: ResetCrossSigning, +}); + +declare global { + interface Window { + // Synapse may fling the user here via UIA fallback, + // this is part of the API to signal completion to the calling client + // https://spec.matrix.org/v1.11/client-server-api/#fallback + onAuthDone?(): void; + } +} + +const ALLOW_CROSS_SIGING_RESET_MUTATION = graphql(/* GraphQL */ ` + mutation AllowCrossSigningReset($userId: ID!) { + allowUserCrossSigningReset(input: { userId: $userId }) { + user { + id + } + } + } +`); + +function ResetCrossSigning(): React.ReactNode { + const { deepLink } = Route.useSearch(); + const navigate = Route.useNavigate(); + const { t } = useTranslation(); + const [viewer] = useQuery({ query: CURRENT_VIEWER_QUERY }); + if (viewer.error) throw viewer.error; + if (viewer.data?.viewer.__typename !== "User") throw notFound(); + const userId = viewer.data.viewer.id; + + const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION); + if (result.error) throw result.error; + const success = !!result.data; + + const onClick = async (): Promise => { + await allowReset({ userId }); + + setTimeout(() => { + // Synapse may fling the user here via UIA fallback, + // this is part of the API to signal completion to the calling client + // https://spec.matrix.org/v1.11/client-server-api/#fallback + if (window.onAuthDone) { + window.onAuthDone(); + } else if (window.opener?.postMessage) { + window.opener.postMessage("authDone", "*"); + } + }); + + navigate({ to: "/reset-cross-signing/success", replace: true }); + }; + + return ( + <> + + + + {t("frontend.reset_cross_signing.description")} + + + + + + + + + + {t("frontend.reset_cross_signing.warning")} + + + + + {deepLink ? ( + + {t("action.cancel")} + + ) : ( + + {t("action.back")} + + )} + + ); +} diff --git a/frontend/src/routes/reset-cross-signing.lazy.tsx b/frontend/src/routes/reset-cross-signing.lazy.tsx deleted file mode 100644 index af15d4950..000000000 --- a/frontend/src/routes/reset-cross-signing.lazy.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2024 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 { createLazyFileRoute, notFound } from "@tanstack/react-router"; -import IconCheck from "@vector-im/compound-design-tokens/assets/web/icons/check"; -import IconCheckCircleSolid from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; -import IconClose from "@vector-im/compound-design-tokens/assets/web/icons/close"; -import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; -import IconKeyOffSolid from "@vector-im/compound-design-tokens/assets/web/icons/key-off-solid"; -import { Button, Text } from "@vector-im/compound-web"; -import { - type ForwardRefExoticComponent, - type MouseEvent, - type RefAttributes, - type SVGProps, - useState, -} from "react"; -import { useTranslation } from "react-i18next"; -import { useMutation, useQuery } from "urql"; - -import BlockList from "../components/BlockList"; -import { ButtonLink } from "../components/ButtonLink"; -import Layout from "../components/Layout"; -import LoadingSpinner from "../components/LoadingSpinner"; -import PageHeading from "../components/PageHeading"; -import { - VisualList, - VisualListItem, -} from "../components/VisualList/VisualList"; -import { graphql } from "../gql"; - -import { CURRENT_VIEWER_QUERY } from "./reset-cross-signing"; - -declare global { - interface Window { - // Synapse may fling the user here via UIA fallback, - // this is part of the API to signal completion to the calling client - // https://spec.matrix.org/v1.11/client-server-api/#fallback - onAuthDone?(): void; - } -} - -const ALLOW_CROSS_SIGING_RESET_MUTATION = graphql(/* GraphQL */ ` - mutation AllowCrossSigningReset($userId: ID!) { - allowUserCrossSigningReset(input: { userId: $userId }) { - user { - id - } - } - } -`); - -export const Route = createLazyFileRoute("/reset-cross-signing")({ - component: ResetCrossSigning, -}); - -// This value comes from Synapse and we have no way to query it from here -// https://github.com/element-hq/synapse/blob/34b758644611721911a223814a7b35d8e14067e6/synapse/rest/admin/users.py#L1335 -const CROSS_SIGNING_REPLACEMENT_PERIOD_MS = 10 * 60 * 1000; // 10 minutes - -function ResetCrossSigning(): React.ReactNode { - const { deepLink } = Route.useSearch(); - const { t } = useTranslation(); - const [viewer] = useQuery({ query: CURRENT_VIEWER_QUERY }); - if (viewer.error) throw viewer.error; - if (viewer.data?.viewer.__typename !== "User") throw notFound(); - const userId = viewer.data.viewer.id; - - const [result, allowReset] = useMutation(ALLOW_CROSS_SIGING_RESET_MUTATION); - const success = !!result.data && !result.error; - const error = !success && result.error; - - const onClick = async (): Promise => { - await allowReset({ userId }); - setTimeout(() => { - // Synapse may fling the user here via UIA fallback, - // this is part of the API to signal completion to the calling client - // https://spec.matrix.org/v1.11/client-server-api/#fallback - if (window.onAuthDone) { - window.onAuthDone(); - } else if (window.opener?.postMessage) { - window.opener.postMessage("authDone", "*"); - } - }); - }; - - const [cancelled, setCancelled] = useState(false); - - let cancelButton: React.ReactNode; - if (!deepLink) { - cancelButton = ( - - {t("action.back")} - - ); - } else if (!success && !error && !cancelled) { - // Only show the back button for a deep link if the user hasn't yet completed the interaction - cancelButton = ( - - ); - } - - let Icon: ForwardRefExoticComponent< - Omit, "ref" | "children"> & - RefAttributes - >; - let title: string; - let body: JSX.Element; - - if (cancelled) { - Icon = IconKeyOffSolid; - title = t("frontend.reset_cross_signing.cancelled.heading"); - body = ( - <> - - {t("frontend.reset_cross_signing.cancelled.description_1")} - - - {t("frontend.reset_cross_signing.cancelled.description_2")} - - - ); - } else if (success) { - Icon = IconCheckCircleSolid; - title = t("frontend.reset_cross_signing.success.heading"); - body = ( - - {t("frontend.reset_cross_signing.success.description", { - minutes: CROSS_SIGNING_REPLACEMENT_PERIOD_MS / (60 * 1000), - })} - - ); - } else if (error) { - Icon = IconError; - title = t("frontend.reset_cross_signing.failure.heading"); - body = ( - - {t("frontend.reset_cross_signing.failure.description")} - - ); - } else { - Icon = IconError; - title = t("frontend.reset_cross_signing.heading"); - body = ( - <> - - {t("frontend.reset_cross_signing.description")} - - - - - - - - {t("frontend.reset_cross_signing.warning")} - - - - ); - } - - return ( - - - - - {body} - {cancelButton} - - - ); -} diff --git a/frontend/src/routes/reset-cross-signing.success.tsx b/frontend/src/routes/reset-cross-signing.success.tsx new file mode 100644 index 000000000..0cbb153ab --- /dev/null +++ b/frontend/src/routes/reset-cross-signing.success.tsx @@ -0,0 +1,34 @@ +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +import { createFileRoute } from "@tanstack/react-router"; +import IconCheckCircleSolid from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; +import { Text } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; +import PageHeading from "../components/PageHeading"; + +// This value comes from Synapse and we have no way to query it from here +// https://github.com/element-hq/synapse/blob/34b758644611721911a223814a7b35d8e14067e6/synapse/rest/admin/users.py#L1335 +const CROSS_SIGNING_REPLACEMENT_PERIOD_MS = 10 * 60 * 1000; // 10 minutes + +export const Route = createFileRoute("/reset-cross-signing/success")({ + component: () => { + const { t } = useTranslation(); + return ( + <> + + + {t("frontend.reset_cross_signing.success.description", { + minutes: CROSS_SIGNING_REPLACEMENT_PERIOD_MS / (60 * 1000), + })} + + + ); + }, +}); diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx index 65ef208a4..d1f55bc7b 100644 --- a/frontend/src/routes/reset-cross-signing.tsx +++ b/frontend/src/routes/reset-cross-signing.tsx @@ -1,40 +1,58 @@ // Copyright 2024 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 { createFileRoute, notFound } from "@tanstack/react-router"; +import { + type ErrorComponentProps, + Outlet, + createFileRoute, +} from "@tanstack/react-router"; import { zodSearchValidator } from "@tanstack/router-zod-adapter"; +import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"; +import { Button, Text } from "@vector-im/compound-web"; import * as z from "zod"; -import { graphql } from "../gql"; +import { useTranslation } from "react-i18next"; +import BlockList from "../components/BlockList"; +import Layout from "../components/Layout"; +import PageHeading from "../components/PageHeading"; const searchSchema = z.object({ deepLink: z.boolean().optional(), }); -export const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ ` - query CurrentViewerQuery { - viewer { - __typename - ... on Node { - id - } - } - } -`); - export const Route = createFileRoute("/reset-cross-signing")({ - async loader({ context, abortController: { signal } }) { - const viewer = await context.client.query( - CURRENT_VIEWER_QUERY, - {}, - { fetchOptions: { signal } }, - ); - if (viewer.error) throw viewer.error; - if (viewer.data?.viewer.__typename !== "User") throw notFound(); - }, - + component: () => ( + + + + + + ), + errorComponent: ResetCrossSigningError, validateSearch: zodSearchValidator(searchSchema), }); + +function ResetCrossSigningError({ + reset, +}: ErrorComponentProps): React.ReactElement { + const { t } = useTranslation(); + return ( + <> + + + + {t("frontend.reset_cross_signing.failure.description")} + + + + + ); +} diff --git a/frontend/src/utils/password_complexity/index.ts b/frontend/src/utils/password_complexity/index.ts index 45d1d6744..ba01af2e0 100644 --- a/frontend/src/utils/password_complexity/index.ts +++ b/frontend/src/utils/password_complexity/index.ts @@ -70,7 +70,7 @@ export interface PasswordComplexity { /** Estimates the complexity of a password. */ export async function estimatePasswordComplexity( password: string, - t: TFunction<"frontend", undefined>, + t: TFunction, ): Promise { const scorerResult = await zxcvbnAsync(password); @@ -96,10 +96,7 @@ export async function estimatePasswordComplexity( } /** Returns a translated string corresponding to the 0 to 4 score. */ -function translateScore( - score: 0 | 1 | 2 | 3 | 4, - t: TFunction<"frontend", undefined>, -): string { +function translateScore(score: 0 | 1 | 2 | 3 | 4, t: TFunction): string { switch (score) { case 0: return t("frontend.password_strength.score.0"); @@ -117,7 +114,7 @@ function translateScore( /** Returns a translated string corresponding to a password improvement suggestion from zxcvbn-ts. */ function translateSuggestion( suggestionCode: string, - t: TFunction<"frontend", undefined>, + t: TFunction, ): string | undefined { switch (suggestionCode) { case "allUppercase": @@ -154,7 +151,7 @@ function translateSuggestion( /** Returns a translated string corresponding to a weak password warning from zxcvbn-ts. */ function translateWarning( warningCode: string, - t: TFunction<"frontend", undefined>, + t: TFunction, ): string | undefined { switch (warningCode) { case "commonNames":