diff --git a/res/css/_components.scss b/res/css/_components.scss index d1576d6ec8e4..734dcc738bb1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -57,6 +57,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"; diff --git a/res/css/structures/auth/_Registration.scss b/res/css/structures/auth/_Registration.scss new file mode 100644 index 000000000000..9f37ec0180e0 --- /dev/null +++ b/res/css/structures/auth/_Registration.scss @@ -0,0 +1,37 @@ +.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; + } +} diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 8c6b6bdff003..487e08fe4402 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -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; diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index e3409792f032..b1399444df83 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -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; } } diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index a37683935a76..6ca7f5d9cfea 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -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; } diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg index 19b8f8244987..126fff6dd3cb 100644 --- a/res/img/element-icons/email-prompt.svg +++ b/res/img/element-icons/email-prompt.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 94c54b32e32b..0c1b326a8020 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -23,7 +23,7 @@ import { IStageStatus, } from "matrix-js-sdk/src/interactive-auth"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import React, { createRef } from 'react'; +import React, { createRef, ReactNode } from 'react'; import { logger } from "matrix-js-sdk/src/logger"; import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents'; @@ -52,6 +52,7 @@ interface IProps { // continueText and continueKind are passed straight through to the AuthEntryComponent. continueText?: string; continueKind?: string; + serverPicker?: ReactNode; // callback makeRequest(auth: IAuthData): Promise; // callback called when the auth process has finished, @@ -269,9 +270,11 @@ export default class InteractiveAuthComponent extends React.Component ); } diff --git a/src/components/structures/auth/AuthHeaderContext.tsx b/src/components/structures/auth/AuthHeaderContext.tsx new file mode 100644 index 000000000000..ac034ee170cf --- /dev/null +++ b/src/components/structures/auth/AuthHeaderContext.tsx @@ -0,0 +1,107 @@ +/* +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, { + createContext, Dispatch, + Fragment, + PropsWithChildren, + ReactNode, + Reducer, + ReducerState, + useContext, + useEffect, + useReducer, +} from "react"; + +interface AuthHeaderContextValue { + title: ReactNode; + icon?: ReactNode; + hideServerPicker?: boolean; +} + +enum AuthHeaderContextActionType { + ADD, + REMOVE +} + +interface AuthHeaderContextAction { + type: AuthHeaderContextActionType; + value: AuthHeaderContextValue; +} + +type AuthContextReducer = Reducer; + +interface AuthHeaderContextType { + state: ReducerState; + dispatch: Dispatch; +} + +const AuthHeaderContext = createContext(undefined); + +export function AuthHeader(content: AuthHeaderContextValue) { + const context = useContext(AuthHeaderContext); + const dispatch = context ? context.dispatch : null; + useEffect(() => { + if (!dispatch) { + return; + } + dispatch({ type: AuthHeaderContextActionType.ADD, value: content }); + return () => dispatch({ type: AuthHeaderContextActionType.REMOVE, value: content }); + }, [content, dispatch]); + return null; +} + +interface Props { + title: ReactNode; + icon?: ReactNode; + serverPicker: ReactNode; +} + +export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren) { + const context = useContext(AuthHeaderContext); + if (!context) { + return null; + } + const current = context.state.length ? context.state[0] : null; + return ( + + { current?.icon ?? icon } +

{ current?.title ?? title }

+ { children } + { current?.hideServerPicker !== true && serverPicker } + + ); +} + +export function AuthHeaderProvider({ children }: PropsWithChildren<{}>) { + const [state, dispatch] = useReducer( + (state: AuthHeaderContextValue[], action: AuthHeaderContextAction) => { + switch (action.type) { + case AuthHeaderContextActionType.ADD: + return [action.value, ...state]; + case AuthHeaderContextActionType.REMOVE: + return (state.length && isEqual(state[0], action.value)) ? state.slice(1) : state; + } + }, + [] as AuthHeaderContextValue[], + ); + return ( + + { children } + + ); +} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index e694fdee40b1..c9af766150e2 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -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"; @@ -25,6 +25,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import * as Lifecycle from '../../../Lifecycle'; import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; +import { AuthHeaderDisplay, AuthHeaderProvider } from "./AuthHeaderContext"; import AuthPage from "../../views/auth/AuthPage"; import Login, { ISSOFlow } from "../../../Login"; import dis from "../../../dispatcher/dispatcher"; @@ -619,28 +620,37 @@ export default class Registration extends React.Component { { regDoneText } ; } else { - body =
-

{ _t('Create account') }

- { errorText } - { serverDeadSection } - - { this.renderRegisterComponent() } - { goBack } - { signIn } -
; + body = +
+ } + > + { errorText } + { serverDeadSection } + + { this.renderRegisterComponent() } +
+
+ { goBack } + { signIn } +
+
; } return ( - - { body } - + + + { body } + + ); } diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx index 4532ceeaf44a..f81e706d1033 100644 --- a/src/components/views/auth/AuthBody.tsx +++ b/src/components/views/auth/AuthBody.tsx @@ -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
- { this.props.children } -
; - } +interface Props { + flex?: boolean; +} + +export default function AuthBody({ flex, children }: PropsWithChildren) { + return
+ { children } +
; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 11a28d1e05d8..0e7b4538ea43 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react'; +import React, { ChangeEvent, createRef, FormEvent, Fragment, MouseEvent, ReactNode } 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'; @@ -22,11 +22,13 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; +import { AuthHeader } from "../../structures/auth/AuthHeaderContext"; import AccessibleButton from "../elements/AccessibleButton"; import Spinner from "../elements/Spinner"; import { LocalisedPolicy, Policies } from '../../../Terms'; import Field from '../elements/Field'; import CaptchaForm from "./CaptchaForm"; +import EmailPromptIcon from '../../../../res/img/element-icons/email-prompt.svg'; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -45,6 +47,9 @@ import CaptchaForm from "./CaptchaForm"; * stageParams: params from the server for the stage being attempted * errorText: error message from a previous attempt to authenticate * submitAuthDict: a function which will be called with the new auth dict + * serverPicker: the UI element allowing you to choose which server to + * authenticate against. In certain stages of the flow, it may + * make sense to avoid showing it. * busy: a boolean indicating whether the auth logic is doing something * the user needs to wait for. * inputs: Object of inputs provided by the user, as in js-sdk @@ -82,10 +87,12 @@ interface IAuthEntryProps { authSessionId: string; errorText?: string; errorCode?: string; + serverPicker?: ReactNode; // Is the auth logic currently waiting for something to happen? busy?: boolean; onPhaseChange: (phase: number) => void; submitAuthDict: (auth: IAuthDict) => void; + requestEmailToken?: () => Promise; } interface IPasswordAuthEntryState { @@ -159,24 +166,27 @@ export class PasswordAuthEntry extends React.Component -

{ _t("Confirm your identity by entering your account password below.") }

-
- - { errorSection } -
- { submitButtonOrSpinner } -
- - + + { this.props.serverPicker } +
+

{ _t("Confirm your identity by entering your account password below.") }

+
+ + { errorSection } +
+ { submitButtonOrSpinner } +
+ +
+
); } } @@ -205,7 +215,12 @@ export class RecaptchaAuthEntry extends React.Component; + return ( + + { this.props.serverPicker } + + + ); } let errorText = this.props.errorText; @@ -230,12 +245,15 @@ export class RecaptchaAuthEntry extends React.Component - - { errorSection } - + + { this.props.serverPicker } +
+ + { errorSection } +
+
); } } @@ -349,7 +367,12 @@ export class TermsAuthEntry extends React.Component; + return ( + + { this.props.serverPicker } + + + ); } const checkboxes = []; @@ -386,12 +409,15 @@ export class TermsAuthEntry extends React.Component -

{ _t("Please review and accept the policies of this homeserver:") }

- { checkboxes } - { errorSection } - { submitButton } - + + { this.props.serverPicker } +
+

{ _t("Please review and accept the policies of this homeserver:") }

+ { checkboxes } + { errorSection } + { submitButton } +
+
); } } @@ -405,9 +431,24 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps { }; } -export class EmailIdentityAuthEntry extends React.Component { +interface IEmailIdentityAuthEntryState { + requested: boolean; + requesting: boolean; +} + +export class EmailIdentityAuthEntry extends + React.Component { static LOGIN_TYPE = AuthType.Email; + constructor(props: IEmailIdentityAuthEntryProps) { + super(props); + + this.state = { + requested: false, + requesting: false, + }; + } + componentDidMount() { this.props.onPhaseChange(DEFAULT_PHASE); } @@ -440,11 +481,54 @@ export class EmailIdentityAuthEntry extends React.Component -

{ _t("A confirmation email has been sent to %(emailAddress)s", + } + hideServerPicker={true} + /> +

{ _t("To create your account, open the link in the email we just sent to %(emailAddress)s.", { emailAddress: { this.props.inputs.emailAddress } }, + ) }

+ { this.state.requesting ? ( +

{ _t("Did not receive it? Resend it", {}, { + a: (text: string) => + null} + disabled + >{ text } + + , + }) }

+ ) : this.state.requested ? ( +

{ _t("Did not receive it? Requested", {}, { + a: (text: string) => null} + disabled + >{ text }, + }) }

+ ) : ( +

{ _t("Did not receive it? Resend it", {}, { + a: (text: string) => { + 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 }, + }) }

) } -

-

{ _t("Open the link in the email to continue registration.") }

{ errorSection } ); @@ -560,7 +644,12 @@ export class MsisdnAuthEntry extends React.Component; + return ( + + { this.props.serverPicker } + + + ); } else { const enableSubmit = Boolean(this.state.token); const submitClasses = classNames({ @@ -576,31 +665,34 @@ export class MsisdnAuthEntry extends React.Component -

{ _t("A text message has been sent to %(msisdn)s", - { msisdn: { this.msisdn } }, - ) } -

-

{ _t("Please enter the code it contains:") }

-
-
- -
- -
- { errorSection } + + { this.props.serverPicker } +
+

{ _t("A text message has been sent to %(msisdn)s", + { msisdn: { this.msisdn } }, + ) } +

+

{ _t("Please enter the code it contains:") }

+
+
+ +
+ +
+ { errorSection } +
-
+ ); } } @@ -726,13 +818,16 @@ export class SSOAuthEntry extends React.Component - { errorSection } -
- { cancelButton } - { continueButton } -
- ; + return ( + + { this.props.serverPicker } + { errorSection } +
+ { cancelButton } + { continueButton } +
+
+ ); } } @@ -796,12 +891,15 @@ export class FallbackAuthEntry extends React.Component { ); } return ( -
- { - _t("Start authentication") - } - { errorSection } -
+ + { this.props.serverPicker } +
+ { + _t("Start authentication") + } + { errorSection } +
+
); } } @@ -814,9 +912,11 @@ export interface IStageComponentProps extends IAuthEntryProps { showContinue?: boolean; continueText?: string; continueKind?: string; + serverPicker?: ReactNode; fail?(e: Error): void; setEmailSid?(sid: string): void; onCancel?(): void; + requestEmailToken?(): Promise; } export interface IStageComponent extends React.ComponentClass> { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 767a5ca82bb1..8e9425b319ac 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2979,8 +2979,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? Resend it": "Did not receive it? Resend it", + "Did not receive it? Requested": "Did not receive it? Requested", "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:",