registration: redesign email verification page (#8554)

This commit is contained in:
Janne Mareike Koschinski 2022-05-13 16:10:22 +02:00 committed by GitHub
parent 438e66bb3f
commit 6d6cfcde11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 382 additions and 90 deletions

View file

@ -269,6 +269,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
setEmailSid={this.setEmailSid}
showContinue={!this.props.continueIsManaged}
onPhaseChange={this.onPhaseChange}
requestEmailToken={this.authLogic.requestEmailToken}
continueText={this.props.continueText}
continueKind={this.props.continueKind}
onCancel={this.onStageCancel}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { createClient } from 'matrix-js-sdk/src/matrix';
import React, { ReactNode } from 'react';
import React, { Fragment, ReactNode } from 'react';
import { MatrixClient } from "matrix-js-sdk/src/client";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
@ -36,6 +36,8 @@ import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner";
import { AuthHeaderDisplay } from './header/AuthHeaderDisplay';
import { AuthHeaderProvider } from './header/AuthHeaderProvider';
interface IProps {
serverConfig: ValidatedServerConfig;
@ -619,28 +621,37 @@ export default class Registration extends React.Component<IProps, IState> {
{ regDoneText }
</div>;
} else {
body = <div>
<h2>{ _t('Create account') }</h2>
{ errorText }
{ serverDeadSection }
<ServerPicker
title={_t("Host account on")}
dialogTitle={_t("Decide where your account is hosted")}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
/>
{ this.renderRegisterComponent() }
{ goBack }
{ signIn }
</div>;
body = <Fragment>
<div className="mx_Register_mainContent">
<AuthHeaderDisplay
title={_t('Create account')}
serverPicker={<ServerPicker
title={_t("Host account on")}
dialogTitle={_t("Decide where your account is hosted")}
serverConfig={this.props.serverConfig}
onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
/>}
>
{ errorText }
{ serverDeadSection }
</AuthHeaderDisplay>
{ this.renderRegisterComponent() }
</div>
<div className="mx_Register_footerActions">
{ goBack }
{ signIn }
</div>
</Fragment>;
}
return (
<AuthPage>
<AuthHeader />
<AuthBody>
{ body }
</AuthBody>
<AuthHeaderProvider>
<AuthBody flex>
{ body }
</AuthBody>
</AuthHeaderProvider>
</AuthPage>
);
}

View file

@ -0,0 +1,26 @@
/*
Copyright 2022 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 { createContext, Dispatch, ReducerAction, ReducerState } from "react";
import type { AuthHeaderReducer } from "./AuthHeaderProvider";
interface AuthHeaderContextType {
state: ReducerState<AuthHeaderReducer>;
dispatch: Dispatch<ReducerAction<AuthHeaderReducer>>;
}
export const AuthHeaderContext = createContext<AuthHeaderContextType>(undefined);

View file

@ -0,0 +1,41 @@
/*
Copyright 2022 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 React, { Fragment, PropsWithChildren, ReactNode, useContext } from "react";
import { AuthHeaderContext } from "./AuthHeaderContext";
interface Props {
title: ReactNode;
icon?: ReactNode;
serverPicker: ReactNode;
}
export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren<Props>) {
const context = useContext(AuthHeaderContext);
if (!context) {
return null;
}
const current = context.state.length ? context.state[0] : null;
return (
<Fragment>
{ current?.icon ?? icon }
<h2>{ current?.title ?? title }</h2>
{ children }
{ current?.hideServerPicker !== true && serverPicker }
</Fragment>
);
}

View file

@ -0,0 +1,39 @@
/*
Copyright 2022 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 { ReactNode, useContext, useEffect } from "react";
import { AuthHeaderContext } from "./AuthHeaderContext";
import { AuthHeaderActionType } from "./AuthHeaderProvider";
interface Props {
title: ReactNode;
icon?: ReactNode;
hideServerPicker?: boolean;
}
export function AuthHeaderModifier(props: Props) {
const context = useContext(AuthHeaderContext);
const dispatch = context ? context.dispatch : null;
useEffect(() => {
if (!dispatch) {
return;
}
dispatch({ type: AuthHeaderActionType.Add, value: props });
return () => dispatch({ type: AuthHeaderActionType.Remove, value: props });
}, [props, dispatch]);
return null;
}

View file

@ -0,0 +1,52 @@
/*
Copyright 2022 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 { isEqual } from "lodash";
import React, { ComponentProps, PropsWithChildren, Reducer, useReducer } from "react";
import { AuthHeaderContext } from "./AuthHeaderContext";
import { AuthHeaderModifier } from "./AuthHeaderModifier";
export enum AuthHeaderActionType {
Add,
Remove
}
interface AuthHeaderAction {
type: AuthHeaderActionType;
value: ComponentProps<typeof AuthHeaderModifier>;
}
export type AuthHeaderReducer = Reducer<ComponentProps<typeof AuthHeaderModifier>[], AuthHeaderAction>;
export function AuthHeaderProvider({ children }: PropsWithChildren<{}>) {
const [state, dispatch] = useReducer<AuthHeaderReducer>(
(state: ComponentProps<typeof AuthHeaderModifier>[], action: AuthHeaderAction) => {
switch (action.type) {
case AuthHeaderActionType.Add:
return [action.value, ...state];
case AuthHeaderActionType.Remove:
return (state.length && isEqual(state[0], action.value)) ? state.slice(1) : state;
}
},
[] as ComponentProps<typeof AuthHeaderModifier>[],
);
return (
<AuthHeaderContext.Provider value={{ state, dispatch }}>
{ children }
</AuthHeaderContext.Provider>
);
}

View file

@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from "classnames";
import React, { PropsWithChildren } from 'react';
export default class AuthBody extends React.PureComponent {
public render(): React.ReactNode {
return <div className="mx_AuthBody">
{ this.props.children }
</div>;
}
interface Props {
flex?: boolean;
}
export default function AuthBody({ flex, children }: PropsWithChildren<Props>) {
return <div className={classNames("mx_AuthBody", { "mx_AuthBody_flex": flex })}>
{ children }
</div>;
}

View file

@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client";
import { AuthType, IAuthDict, IInputs, IStageStatus } from 'matrix-js-sdk/src/interactive-auth';
import { logger } from "matrix-js-sdk/src/logger";
import React, { ChangeEvent, createRef, FormEvent, Fragment, MouseEvent } from 'react';
import EmailPromptIcon from '../../../../res/img/element-icons/email-prompt.svg';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
import { LocalisedPolicy, Policies } from '../../../Terms';
import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier";
import AccessibleButton from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import Field from '../elements/Field';
import Spinner from "../elements/Spinner";
import { Alignment } from "../elements/Tooltip";
import CaptchaForm from "./CaptchaForm";
/* This file contains a collection of components which are used by the
@ -86,6 +90,7 @@ interface IAuthEntryProps {
busy?: boolean;
onPhaseChange: (phase: number) => void;
submitAuthDict: (auth: IAuthDict) => void;
requestEmailToken?: () => Promise<void>;
}
interface IPasswordAuthEntryState {
@ -205,7 +210,9 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
render() {
if (this.props.busy) {
return <Spinner />;
return (
<Spinner />
);
}
let errorText = this.props.errorText;
@ -349,7 +356,9 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
render() {
if (this.props.busy) {
return <Spinner />;
return (
<Spinner />
);
}
const checkboxes = [];
@ -405,9 +414,24 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
};
}
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
interface IEmailIdentityAuthEntryState {
requested: boolean;
requesting: boolean;
}
export class EmailIdentityAuthEntry extends
React.Component<IEmailIdentityAuthEntryProps, IEmailIdentityAuthEntryState> {
static LOGIN_TYPE = AuthType.Email;
constructor(props: IEmailIdentityAuthEntryProps) {
super(props);
this.state = {
requested: false,
requesting: false,
};
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
@ -440,11 +464,51 @@ export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEn
} else {
return (
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
<AuthHeaderModifier
title={_t("Check your email to continue")}
icon={<img
src={EmailPromptIcon}
alt={_t("Unread email icon")}
width={16}
/>}
hideServerPicker={true}
/>
<p>{ _t("To create your account, open the link in the email we just sent to %(emailAddress)s.",
{ emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
) }
</p>
<p>{ _t("Open the link in the email to continue registration.") }</p>
) }</p>
{ this.state.requesting ? (
<p className="secondary">{ _t("Did not receive it? <a>Resend it</a>", {}, {
a: (text: string) => <Fragment>
<AccessibleButton
kind='link_inline'
onClick={() => null}
disabled
>{ text } <Spinner w={14} h={14} /></AccessibleButton>
</Fragment>,
}) }</p>
) : <p className="secondary">{ _t("Did not receive it? <a>Resend it</a>", {}, {
a: (text: string) => <AccessibleTooltipButton
kind='link_inline'
title={this.state.requested
? _t("Resent!")
: _t("Resend")}
alignment={Alignment.Right}
tooltipClassName="mx_Tooltip_noMargin"
onHideTooltip={this.state.requested
? () => this.setState({ requested: false })
: undefined}
onClick={async () => {
this.setState({ requesting: true });
try {
await this.props.requestEmailToken?.();
} catch (e) {
logger.warn("Email token request failed: ", e);
} finally {
this.setState({ requested: true, requesting: false });
}
}}
>{ text }</AccessibleTooltipButton>,
}) }</p> }
{ errorSection }
</div>
);
@ -560,7 +624,9 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
render() {
if (this.state.requestingToken) {
return <Spinner />;
return (
<Spinner />
);
} else {
const enableSubmit = Boolean(this.state.token);
const submitClasses = classNames({
@ -726,13 +792,15 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
);
}
return <React.Fragment>
{ errorSection }
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
{ cancelButton }
{ continueButton }
</div>
</React.Fragment>;
return (
<Fragment>
{ errorSection }
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
{ cancelButton }
{ continueButton }
</div>
</Fragment>
);
}
}
@ -817,6 +885,7 @@ export interface IStageComponentProps extends IAuthEntryProps {
fail?(e: Error): void;
setEmailSid?(sid: string): void;
onCancel?(): void;
requestEmailToken?(): Promise<void>;
}
export interface IStageComponent extends React.ComponentClass<React.PropsWithRef<IStageComponentProps>> {