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

@ -58,6 +58,7 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
@import "./structures/auth/_Registration.scss";
@import "./structures/auth/_SetupEncryptionBody.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";

View file

@ -0,0 +1,53 @@
/*
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.
*/
.mx_Register_mainContent {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 270px;
p {
font-size: $font-14px;
color: $authpage-primary-color;
&.secondary {
color: $authpage-secondary-color;
}
}
> img:first-child {
margin-bottom: 16px;
width: max-content;
}
.mx_Login_submit {
margin-bottom: 0;
}
}
.mx_Register_footerActions {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid rgba(141, 151, 165, 0.2);
> * {
flex-basis: content;
}
}

View file

@ -24,6 +24,11 @@ limitations under the License.
padding: 25px 60px;
box-sizing: border-box;
&.mx_AuthBody_flex {
display: flex;
flex-direction: column;
}
h2 {
font-size: $font-24px;
font-weight: 600;
@ -139,7 +144,6 @@ limitations under the License.
.mx_AuthBody_changeFlow {
display: block;
text-align: center;
width: 100%;
> a {
font-weight: $font-semi-bold;

View file

@ -28,10 +28,12 @@ limitations under the License.
border-radius: 4px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33);
background-color: $authpage-modal-bg-color;
}
@media only screen and (max-width: 480px) {
.mx_AuthPage_modal {
@media only screen and (max-height: 768px) {
margin-top: 50px;
}
@media only screen and (max-width: 480px) {
margin-top: 0;
}
}

View file

@ -14,35 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_InteractiveAuthEntryComponents_emailWrapper {
padding-right: 100px;
position: relative;
margin-top: 32px;
margin-bottom: 32px;
&::before, &::after {
position: absolute;
width: 116px;
height: 116px;
content: "";
right: -10px;
}
&::before {
background-color: rgba(244, 246, 250, 0.91);
border-radius: 50%;
top: -20px;
}
&::after {
background-image: url('$(res)/img/element-icons/email-prompt.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
top: -25px;
}
}
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
text-align: center;
}
@ -103,3 +74,21 @@ limitations under the License.
margin-left: 5px;
}
}
.mx_InteractiveAuthEntryComponents_emailWrapper {
// "Resend" button/link
.mx_AccessibleButton_kind_link_inline {
// We need this to be an inline-block so positioning works correctly
display: inline-block !important;
// Spinner as end adornment of the "resend" button/link
.mx_Spinner {
// Spinners are usually block elements, but we need it as inline element
display: inline-flex !important;
// Spinners by default fill all available width, but we don't want that
width: auto !important;
// We need to center the spinner relative to the button/link
vertical-align: middle !important;
}
}
}

View file

@ -76,6 +76,11 @@ limitations under the License.
border: 0;
text-align: center;
&:not(.mx_Tooltip_noMargin) {
margin-left: 6px;
margin-right: 6px;
}
.mx_Tooltip_chevron {
display: none;
}

View file

@ -1,13 +1,6 @@
<svg width="57" height="77" viewBox="0 0 57 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55298 38.9352H4C1.79086 38.9352 0 40.726 0 42.9352V72.0304C0 74.2396 1.79086 76.0304 4 76.0304H53C55.2091 76.0304 57 74.2396 57 72.0304V42.9352C57 40.726 55.2091 38.9352 53 38.9352H51.365V41.6473H5.55298V38.9352ZM26.9753 61.3068L3.10141 43.4482C2.33137 42.8721 2.73876 41.6474 3.70041 41.6474H28.459H53.3841C54.3282 41.6474 54.7464 42.8352 54.0107 43.4268L31.8776 61.2212C30.4545 62.3653 28.4374 62.4005 26.9753 61.3068Z" fill="#8A8C8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.5885 33.0898C48.9384 33.2156 48.2703 33.2911 47.5885 33.3119V34.706V44.4238V54.1415H49.5885V44.4238V34.706V33.0898ZM36.5604 14.2706H13.7177C10.9562 14.2706 8.71765 16.5092 8.71765 19.2706V34.706V44.4238V54.1415H10.7177V44.4238V34.706V19.2706C10.7177 17.6138 12.0608 16.2706 13.7177 16.2706H35.5616C35.8354 15.571 36.1706 14.9022 36.5604 14.2706Z" fill="#8A8C8E"/>
<path d="M16.6589 30.5414H37.4826" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16.2706" y1="37.8708" x2="40.6473" y2="37.8708" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16.2706" y1="44.812" x2="40.6473" y2="44.812" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="47.2003" cy="20.8237" r="9.71771" fill="#FE2928"/>
<rect x="45.812" y="14.5765" width="2.77649" height="8.32946" rx="1" fill="white"/>
<rect x="45.812" y="24.2943" width="2.77649" height="2.77649" rx="1" fill="white"/>
<line x1="27.3766" y1="1" x2="27.3766" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="34.3179" y1="6.55298" x2="34.3179" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="20.4354" y1="6.55298" x2="20.4354" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg viewBox="0 0 57 46" width="57" height="46" xmlns="http://www.w3.org/2000/svg">
<path fill="#737d8c" d="M 4,17 C 3.500492,17 3.024855,17.09642 2.583984,17.263672 L 20,34.679688 37.416016,17.263672 C 36.975147,17.096421 36.499499,17 36,17 Z M 0.263672,19.583984 C 0.096421,20.024855 0,20.500492 0,21 v 21 c 0,2.2091 1.79086,4 4,4 h 32 c 2.2091,0 4,-1.7909 4,-4 V 21 c 0,-0.499508 -0.09642,-0.975145 -0.263672,-1.416016 L 21.160156,38.160156 a 1.640164,1.640164 0 0 1 -0.533203,0.355469 1.640164,1.640164 0 0 1 -0.626953,0.125 1.640164,1.640164 0 0 1 -0.626953,-0.125 1.640164,1.640164 0 0 1 -0.533203,-0.355469 z" />
<path fill="#0dbd8b" fill-opacity="0.1" d="m 57,16 a 16,16 0 0 1 -16,16 16,16 0 0 1 -16,-16 16,16 0 0 1 16,-16 16,16 0 0 1 16,16 z" />
<path fill="#ffffff" d="m 53,16 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z" />
<path fill="#0dbd8b" d="m 49,16 a 8,8 0 0 1 -8,8 8,8 0 0 1 -8,-8 8,8 0 0 1 8,-8 8,8 0 0 1 8,8 z" />
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 994 B

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>> {

View file

@ -2976,8 +2976,11 @@
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
"Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
"Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",
"A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s",
"Open the link in the email to continue registration.": "Open the link in the email to continue registration.",
"Check your email to continue": "Check your email to continue",
"Unread email icon": "Unread email icon",
"To create your account, open the link in the email we just sent to %(emailAddress)s.": "To create your account, open the link in the email we just sent to %(emailAddress)s.",
"Did not receive it? <a>Resend it</a>": "Did not receive it? <a>Resend it</a>",
"Resent!": "Resent!",
"Token incorrect": "Token incorrect",
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:",