Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.

Commit 1f55710

Browse files
authored
Mobile registration optimizations and tests (#62)
* Mobile registration optimizations - don't autocaptialize or autocorrect on username field - show each password field in their own row - improve position of tooltip on mobile so that it's visible * Use optional prop rather than default prop. * Redirect to welcome screen if mobile_registration is requested but not enabled in the config. * autocorrect value should be "off" * Add unit tests for mobile registration * Fix test typo * Fix typo
1 parent 4be5338 commit 1f55710

File tree

9 files changed

+142
-19
lines changed

9 files changed

+142
-19
lines changed

src/components/structures/MatrixChat.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -952,18 +952,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
952952
}
953953

954954
private async startRegistration(params: { [key: string]: string }, isMobileRegistration?: boolean): Promise<void> {
955-
if (!SettingsStore.getValue(UIFeature.Registration)) {
955+
// If registration is disabled or mobile registration is requested but not enabled in settings redirect to the welcome screen
956+
if (
957+
!SettingsStore.getValue(UIFeature.Registration) ||
958+
(isMobileRegistration && !SettingsStore.getValue("Registration.mobileRegistrationHelper"))
959+
) {
956960
this.showScreen("welcome");
957961
return;
958962
}
959-
const isMobileRegistrationAllowed =
960-
isMobileRegistration && SettingsStore.getValue("Registration.mobileRegistrationHelper");
961963

962964
const newState: Partial<IState> = {
963965
view: Views.REGISTER,
964966
};
965967

966-
if (isMobileRegistrationAllowed && params.hs_url) {
968+
if (isMobileRegistration && params.hs_url) {
967969
try {
968970
const config = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(params.hs_url);
969971
newState.serverConfig = config;
@@ -992,12 +994,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
992994
newState.register_id_sid = params.sid;
993995
}
994996

995-
newState.isMobileRegistration = isMobileRegistrationAllowed;
997+
newState.isMobileRegistration = isMobileRegistration;
996998

997999
this.setStateForNewView(newState);
9981000
ThemeController.isLogin = true;
9991001
this.themeWatcher.recheck();
1000-
this.notifyNewScreen(isMobileRegistrationAllowed ? "mobile_register" : "register");
1002+
this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register");
10011003
}
10021004

10031005
// switch view to the given room

src/components/structures/auth/Registration.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ export default class Registration extends React.Component<IProps, IState> {
627627
serverConfig={this.props.serverConfig}
628628
canSubmit={!this.state.serverErrorIsFatal}
629629
matrixClient={this.state.matrixClient}
630+
mobileRegister={this.props.mobileRegister}
630631
/>
631632
</React.Fragment>
632633
);
@@ -779,7 +780,11 @@ export default class Registration extends React.Component<IProps, IState> {
779780
);
780781
}
781782
if (this.props.mobileRegister) {
782-
return <div className="mx_MobileRegister_body">{body}</div>;
783+
return (
784+
<div className="mx_MobileRegister_body" data-testid="mobile-register">
785+
{body}
786+
</div>
787+
);
783788
}
784789
return (
785790
<AuthPage>

src/components/views/auth/EmailField.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Field, { IInputProps } from "../elements/Field";
1212
import { _t, _td, TranslationKey } from "../../../languageHandler";
1313
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
1414
import * as Email from "../../../email";
15+
import { Alignment } from "../elements/Tooltip";
1516

1617
interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
1718
id?: string;
@@ -22,6 +23,7 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
2223
label: TranslationKey;
2324
labelRequired: TranslationKey;
2425
labelInvalid: TranslationKey;
26+
tooltipAlignment?: Alignment;
2527

2628
// When present, completely overrides the default validation rules.
2729
validationRules?: (fieldState: IFieldState) => Promise<IValidationResult>;
@@ -77,6 +79,7 @@ class EmailField extends PureComponent<IProps> {
7779
autoFocus={this.props.autoFocus}
7880
onChange={this.props.onChange}
7981
onValidate={this.onValidate}
82+
tooltipAlignment={this.props.tooltipAlignment}
8083
/>
8184
);
8285
}

src/components/views/auth/PassphraseConfirmField.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import React, { PureComponent, RefCallback, RefObject } from "react";
1111
import Field, { IInputProps } from "../elements/Field";
1212
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
1313
import { _t, _td, TranslationKey } from "../../../languageHandler";
14+
import { Alignment } from "../elements/Tooltip";
1415

1516
interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> {
1617
id?: string;
@@ -22,7 +23,7 @@ interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> {
2223
label: TranslationKey;
2324
labelRequired: TranslationKey;
2425
labelInvalid: TranslationKey;
25-
26+
tooltipAlignment?: Alignment;
2627
onChange(ev: React.FormEvent<HTMLElement>): void;
2728
onValidate?(result: IValidationResult): void;
2829
}
@@ -70,6 +71,7 @@ class PassphraseConfirmField extends PureComponent<IProps> {
7071
onChange={this.props.onChange}
7172
onValidate={this.onValidate}
7273
autoFocus={this.props.autoFocus}
74+
tooltipAlignment={this.props.tooltipAlignment}
7375
/>
7476
);
7577
}

src/components/views/auth/PassphraseField.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import withValidation, { IFieldState, IValidationResult } from "../elements/Vali
1515
import { _t, _td, TranslationKey } from "../../../languageHandler";
1616
import Field, { IInputProps } from "../elements/Field";
1717
import { MatrixClientPeg } from "../../../MatrixClientPeg";
18+
import { Alignment } from "../elements/Tooltip";
1819

1920
interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
2021
autoFocus?: boolean;
@@ -30,6 +31,7 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
3031
labelEnterPassword: TranslationKey;
3132
labelStrongPassword: TranslationKey;
3233
labelAllowedButUnsafe: TranslationKey;
34+
tooltipAlignment?: Alignment;
3335

3436
onChange(ev: React.FormEvent<HTMLElement>): void;
3537
onValidate?(result: IValidationResult): void;
@@ -111,6 +113,7 @@ class PassphraseField extends PureComponent<IProps> {
111113
value={this.props.value}
112114
onChange={this.props.onChange}
113115
onValidate={this.onValidate}
116+
tooltipAlignment={this.props.tooltipAlignment}
114117
/>
115118
);
116119
}

src/components/views/auth/RegistrationForm.tsx

+33-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import RegistrationEmailPromptDialog from "../dialogs/RegistrationEmailPromptDia
2626
import CountryDropdown from "./CountryDropdown";
2727
import PassphraseConfirmField from "./PassphraseConfirmField";
2828
import { PosthogAnalytics } from "../../../PosthogAnalytics";
29+
import { Alignment } from "../elements/Tooltip";
2930

3031
enum RegistrationField {
3132
Email = "field_email",
@@ -58,6 +59,7 @@ interface IProps {
5859
serverConfig: ValidatedServerConfig;
5960
canSubmit?: boolean;
6061
matrixClient: MatrixClient;
62+
mobileRegister?: boolean;
6163

6264
onRegisterClick(params: {
6365
username: string;
@@ -439,6 +441,13 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
439441
return true;
440442
}
441443

444+
private tooltipAlignment(): Alignment | undefined {
445+
if (this.props.mobileRegister) {
446+
return Alignment.Bottom;
447+
}
448+
return undefined;
449+
}
450+
442451
private renderEmail(): ReactNode {
443452
if (!this.showEmail()) {
444453
return null;
@@ -454,6 +463,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
454463
validationRules={this.validateEmailRules.bind(this)}
455464
onChange={this.onEmailChange}
456465
onValidate={this.onEmailValidate}
466+
tooltipAlignment={this.tooltipAlignment()}
457467
/>
458468
);
459469
}
@@ -468,6 +478,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
468478
onChange={this.onPasswordChange}
469479
onValidate={this.onPasswordValidate}
470480
userInputs={[this.state.username]}
481+
tooltipAlignment={this.tooltipAlignment()}
471482
/>
472483
);
473484
}
@@ -482,6 +493,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
482493
password={this.state.password}
483494
onChange={this.onPasswordConfirmChange}
484495
onValidate={this.onPasswordConfirmValidate}
496+
tooltipAlignment={this.tooltipAlignment()}
485497
/>
486498
);
487499
}
@@ -526,6 +538,9 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
526538
value={this.state.username}
527539
onChange={this.onUsernameChange}
528540
onValidate={this.onUsernameValidate}
541+
tooltipAlignment={this.tooltipAlignment()}
542+
autoCorrect="off"
543+
autoCapitalize="none"
529544
/>
530545
);
531546
}
@@ -557,14 +572,28 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
557572
}
558573
}
559574

575+
let passwordFields: JSX.Element | undefined;
576+
if (this.props.mobileRegister) {
577+
passwordFields = (
578+
<>
579+
<div className="mx_AuthBody_fieldRow">{this.renderPassword()}</div>
580+
<div className="mx_AuthBody_fieldRow">{this.renderPasswordConfirm()}</div>
581+
</>
582+
);
583+
} else {
584+
passwordFields = (
585+
<div className="mx_AuthBody_fieldRow">
586+
{this.renderPassword()}
587+
{this.renderPasswordConfirm()}
588+
</div>
589+
);
590+
}
591+
560592
return (
561593
<div>
562594
<form onSubmit={this.onSubmit}>
563595
<div className="mx_AuthBody_fieldRow">{this.renderUsername()}</div>
564-
<div className="mx_AuthBody_fieldRow">
565-
{this.renderPassword()}
566-
{this.renderPasswordConfirm()}
567-
</div>
596+
{passwordFields}
568597
<div className="mx_AuthBody_fieldRow">
569598
{this.renderEmail()}
570599
{this.renderPhoneNumber()}

src/components/views/elements/Field.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import classNames from "classnames";
1717
import { debounce } from "lodash";
1818

1919
import { IFieldState, IValidationResult } from "./Validation";
20-
import Tooltip from "./Tooltip";
20+
import Tooltip, { Alignment } from "./Tooltip";
2121
import { Key } from "../../../Keyboard";
2222

2323
// Invoke validation from user input (when typing, etc.) at most once every N ms.
@@ -60,6 +60,8 @@ interface IProps {
6060
tooltipContent?: React.ReactNode;
6161
// If specified the tooltip will be shown regardless of feedback
6262
forceTooltipVisible?: boolean;
63+
// If specified, the tooltip with be aligned accorindly with the field, defaults to Right.
64+
tooltipAlignment?: Alignment;
6365
// If specified alongside tooltipContent, the class name to apply to the
6466
// tooltip itself.
6567
tooltipClassName?: string;
@@ -261,6 +263,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
261263
validateOnFocus,
262264
usePlaceholderAsHint,
263265
forceTooltipVisible,
266+
tooltipAlignment,
264267
...inputProps
265268
} = this.props;
266269

@@ -286,7 +289,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
286289
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
287290
visible={visible}
288291
label={tooltipContent || this.state.feedback}
289-
alignment={Tooltip.Alignment.Right}
292+
alignment={tooltipAlignment || Alignment.Right}
290293
role={role}
291294
/>
292295
);

test/components/structures/MatrixChat-test.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
5555
import DMRoomMap from "../../../src/utils/DMRoomMap";
5656
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
5757
import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
58+
import { UIFeature } from "../../../src/settings/UIFeature";
5859

5960
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
6061
completeAuthorizationCodeGrant: jest.fn(),
@@ -1462,4 +1463,42 @@ describe("<MatrixChat />", () => {
14621463
});
14631464
});
14641465
});
1466+
1467+
describe("mobile registration", () => {
1468+
const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
1469+
const renderResult = getComponent();
1470+
// wait for welcome page chrome render
1471+
await screen.findByText("powered by Matrix");
1472+
1473+
// go to mobile_register page
1474+
defaultDispatcher.dispatch({
1475+
action: "start_mobile_registration",
1476+
});
1477+
1478+
await flushPromises();
1479+
1480+
return renderResult;
1481+
};
1482+
1483+
const enabledMobileRegistration = (): void => {
1484+
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
1485+
if (settingName === "Registration.mobileRegistrationHelper") return true;
1486+
if (settingName === UIFeature.Registration) return true;
1487+
});
1488+
};
1489+
1490+
it("should render welcome screen if mobile registration is not enabled in settings", async () => {
1491+
await getComponentAndWaitForReady();
1492+
1493+
await screen.findByText("powered by Matrix");
1494+
});
1495+
1496+
it("should render mobile registration", async () => {
1497+
enabledMobileRegistration();
1498+
1499+
await getComponentAndWaitForReady();
1500+
1501+
expect(screen.getByTestId("mobile-register")).toBeInTheDocument();
1502+
});
1503+
});
14651504
});

test/components/structures/auth/Registration-test.tsx

+42-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
88
*/
99

1010
import React from "react";
11-
import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-library/react";
11+
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "@testing-library/react";
1212
import { createClient, MatrixClient, MatrixError, OidcClientConfig } from "matrix-js-sdk/src/matrix";
1313
import { mocked, MockedObject } from "jest-mock";
1414
import fetchMock from "fetch-mock-jest";
@@ -87,12 +87,23 @@ describe("Registration", function () {
8787
const defaultHsUrl = "https://matrix.org";
8888
const defaultIsUrl = "https://vector.im";
8989

90-
function getRawComponent(hsUrl = defaultHsUrl, isUrl = defaultIsUrl, authConfig?: OidcClientConfig) {
91-
return <Registration {...defaultProps} serverConfig={mkServerConfig(hsUrl, isUrl, authConfig)} />;
90+
function getRawComponent(
91+
hsUrl = defaultHsUrl,
92+
isUrl = defaultIsUrl,
93+
authConfig?: OidcClientConfig,
94+
mobileRegister?: boolean,
95+
) {
96+
return (
97+
<Registration
98+
{...defaultProps}
99+
serverConfig={mkServerConfig(hsUrl, isUrl, authConfig)}
100+
mobileRegister={mobileRegister}
101+
/>
102+
);
92103
}
93104

94-
function getComponent(hsUrl?: string, isUrl?: string, authConfig?: OidcClientConfig) {
95-
return render(getRawComponent(hsUrl, isUrl, authConfig));
105+
function getComponent(hsUrl?: string, isUrl?: string, authConfig?: OidcClientConfig, mobileRegister?: boolean) {
106+
return render(getRawComponent(hsUrl, isUrl, authConfig, mobileRegister));
96107
}
97108

98109
it("should show server picker", async function () {
@@ -208,5 +219,31 @@ describe("Registration", function () {
208219
);
209220
});
210221
});
222+
223+
describe("when is mobile registeration", () => {
224+
it("should not show server picker", async function () {
225+
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
226+
expect(container.querySelector(".mx_ServerPicker")).toBeFalsy();
227+
});
228+
229+
it("should show username field with autocaps disabled", async function () {
230+
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
231+
232+
await waitFor(() =>
233+
expect(container.querySelector("#mx_RegistrationForm_username")).toHaveAttribute(
234+
"autocapitalize",
235+
"none",
236+
),
237+
);
238+
});
239+
240+
it("should show password and confirm password fields in separate rows", async function () {
241+
const { container } = getComponent(defaultHsUrl, defaultIsUrl, undefined, true);
242+
243+
await waitFor(() => expect(container.querySelector("#mx_RegistrationForm_username")).toBeTruthy());
244+
// when password and confirm password fields are in separate rows there should be 4 rather than 3
245+
expect(container.querySelectorAll(".mx_AuthBody_fieldRow")).toHaveLength(4);
246+
});
247+
});
211248
});
212249
});

0 commit comments

Comments
 (0)