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.") }
-
-
+
+ { this.props.serverPicker }
+
+
{ _t("Confirm your identity by entering your account password below.") }
+
+
+
);
}
}
@@ -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:",