Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
"src/routeTree.gen.ts",
".storybook/locales.ts",
"i18next.config.ts",
"src/external/**",
],
ignoreDependencies: [
// This is used by the tailwind PostCSS plugin, but not detected by knip
Expand Down
1 change: 1 addition & 0 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"loading": "Loading…",
"next": "Next",
"password": "Password",
"password_confirm": "Confirm password",
"previous": "Previous",
"saved": "Saved",
"saving": "Saving…"
Expand Down
1 change: 1 addition & 0 deletions frontend/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"loading": "Chargement…",
"next": "Suivant",
"password": "Mot de passe",
"password_confirm": "Confirmer le mot de passe",
"previous": "Précédent",
"saved": "Sauvegardé",
"saving": "Enregistrement..."
Expand Down
24 changes: 15 additions & 9 deletions frontend/src/components/PasswordCreationDoubleInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,17 @@ const usePasswordComplexity = (password: string): PasswordComplexity => {
export default function PasswordCreationDoubleInput({
siteConfig,
forceShowNewPasswordInvalid,
newPasswordFieldName,
newPasswordLabel,
newPasswordAgainFieldName,
newPasswordAgainLabel,
}: {
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
forceShowNewPasswordInvalid: boolean;
newPasswordFieldName: string;
newPasswordLabel: string;
newPasswordAgainFieldName: string;
newPasswordAgainLabel: string;
}): React.ReactElement {
const { t } = useTranslation();
const { minimumPasswordComplexity } = useFragment(
Expand All @@ -81,10 +89,8 @@ export default function PasswordCreationDoubleInput({

return (
<>
<Form.Field name="new_password">
<Form.Label>
{t("frontend.password_change.new_password_label")}
</Form.Label>
<Form.Field name={newPasswordFieldName}>
<Form.Label>{newPasswordLabel}</Form.Label>

<Form.PasswordControl
required
Expand Down Expand Up @@ -128,15 +134,13 @@ export default function PasswordCreationDoubleInput({
)}
</Form.Field>

<Form.Field name="new_password_again">
<Form.Field name={newPasswordAgainFieldName}>
{/*
TODO This field has validation defects,
some caused by Radix-UI upstream bugs.
https://github.com/matrix-org/matrix-authentication-service/issues/2855
*/}
<Form.Label>
{t("frontend.password_change.new_password_again_label")}
</Form.Label>
<Form.Label>{newPasswordAgainLabel}</Form.Label>

<Form.PasswordControl
required
Expand All @@ -148,7 +152,9 @@ export default function PasswordCreationDoubleInput({
{t("frontend.errors.field_required")}
</Form.ErrorMessage>

<Form.ErrorMessage match={(v, form) => v !== form.get("new_password")}>
<Form.ErrorMessage
match={(v, form) => v !== form.get(newPasswordFieldName)}
>
{t("frontend.password_change.passwords_no_match")}
</Form.ErrorMessage>

Expand Down
45 changes: 45 additions & 0 deletions frontend/src/external/mount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TooltipProvider } from "@vector-im/compound-web";
import { createElement, StrictMode, Suspense } from "react";
import ReactDOM from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import ErrorBoundary from "../components/ErrorBoundary";
import i18n, { setupI18n } from "../i18n";
import "../shared.css";

setupI18n();

export function mountWithProviders<P = Record<string, unknown>>(
selector: string,
Component: React.ComponentType<P>,
defaultProps?: Partial<P>,
) {
try {
const el = document.querySelector(selector);
if (!el) throw new Error(`can not find ${selector} in DOM`);
const propsJSON = el.getAttribute("data-props") || "{}";
const parsedProps = JSON.parse(propsJSON);
const props = { ...(defaultProps ?? {}), ...(parsedProps ?? {}) };
const queryClient = new QueryClient();
ReactDOM.createRoot(el).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<TooltipProvider>
<Suspense fallback={<div>{`Loading... ${selector}…`}</div>}>
<I18nextProvider i18n={i18n}>
{createElement(
Component as React.ComponentType<any>,
props as P,
)}
</I18nextProvider>
</Suspense>
</TooltipProvider>
</ErrorBoundary>
</QueryClientProvider>
</StrictMode>,
);
} catch (err) {
console.error(`Cannot mount component on ${selector}:`, err);
}
}
61 changes: 61 additions & 0 deletions frontend/src/external/register/PasswordDoubleInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { Form } from "@vector-im/compound-web";
import { graphql } from "../../gql";
import { graphqlRequest } from "../../graphql";
import { mountWithProviders } from "../mount";
import "../../shared.css";
import { useTranslation } from "react-i18next";
import PasswordCreationDoubleInput from "../../components/PasswordCreationDoubleInput";

const HTML_ID = "#password-double-input";

const QUERY = graphql(/* GraphQL */ `
query PasswordChange {
viewer {
__typename
... on Node {
id
}
}

siteConfig {
...PasswordCreationDoubleInput_siteConfig
}
}
`);

const query = queryOptions({
queryKey: ["passwordChange"],
queryFn: ({ signal }) => graphqlRequest({ query: QUERY, signal }),
});

type PasswordDoubleInputProps = { forceShowNewPasswordInvalid: boolean };

function PasswordDoubleInput({
forceShowNewPasswordInvalid,
}: PasswordDoubleInputProps) {
const {
data: { siteConfig },
} = useSuspenseQuery(query);
const { t } = useTranslation();

return (
// Form.Root is needed because Form.Field requires to be included into a Form
// asChild allows to replace Form.Root component by the child, the <form> used is in the password.html
<Form.Root asChild>
<div>
<PasswordCreationDoubleInput
siteConfig={siteConfig}
forceShowNewPasswordInvalid={forceShowNewPasswordInvalid}
newPasswordFieldName="password"
newPasswordLabel={t("common.password")}
newPasswordAgainFieldName="password_confirm"
newPasswordAgainLabel={t("common.password_confirm")}
/>
</div>
</Form.Root>
);
}

// Allow mounting under either the new specific id or the legacy #view
mountWithProviders(HTML_ID, PasswordDoubleInput);
12 changes: 6 additions & 6 deletions frontend/src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Documents = {
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": typeof types.UserEmailList_UserFragmentDoc,
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": typeof types.PasswordChangeDocument,
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": typeof types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
Expand All @@ -62,7 +63,6 @@ type Documents = {
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": typeof types.DoVerifyEmailDocument,
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": typeof types.ResendEmailAuthenticationCodeDocument,
"\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": typeof 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": typeof types.PasswordChangeDocument,
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": typeof types.RecoverPasswordDocument,
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": typeof types.ResendRecoveryEmailDocument,
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": typeof types.RecoverPassword_UserRecoveryTicketFragmentDoc,
Expand Down Expand Up @@ -106,6 +106,7 @@ const documents: Documents = {
"\n fragment UserEmailList_user on User {\n hasPassword\n }\n": types.UserEmailList_UserFragmentDoc,
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeDocument,
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": types.PlanManagementTabDocument,
"\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,
Expand All @@ -119,7 +120,6 @@ const documents: Documents = {
"\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 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,
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": types.ResendRecoveryEmailDocument,
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": types.RecoverPassword_UserRecoveryTicketFragmentDoc,
Expand Down Expand Up @@ -265,6 +265,10 @@ export function graphql(source: "\n fragment UserEmailList_siteConfig on SiteCo
* 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 BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"): typeof import('./graphql').BrowserSessionsOverview_UserFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -317,10 +321,6 @@ export function graphql(source: "\n mutation ResendEmailAuthenticationCode($id:
* 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 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"): typeof import('./graphql').ChangePasswordDocument;
/**
* 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 PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading
Loading