From 07f4fe7bee862129574bae0b023958d9242669fd Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 16:05:53 +0200 Subject: [PATCH 01/15] Clean up isAccountExpired --- src/tchap/util/TchapUtils.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index 2b0c186bc..4c4a400d9 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -6,6 +6,7 @@ import { findMapStyleUrl } from "matrix-react-sdk/src/utils/location"; import TchapApi from "./TchapApi"; import { ClientConfig } from "~tchap-web/yarn-linked-dependencies/matrix-js-sdk/src/autodiscovery"; +import { MatrixError } from "~tchap-web/yarn-linked-dependencies/matrix-js-sdk/src/http-api"; /** * Tchap utils. @@ -220,19 +221,20 @@ export default class TchapUtils { } /** - * Verify if the account is expired. - * It executes an API call and check that it receives a ORG_MATRIX_EXPIRED_ACCOUNT - * The API invoked is getProfileInfo() - * @param matrixId the account matrix Id + * Verify if the currently logged in account is expired. + * It executes an API call (to getProfileInfo) and checks whether the call throws a ORG_MATRIX_EXPIRED_ACCOUNT * @returns true if account is expired, false otherwise */ - static async isAccountExpired(matrixId?: string): Promise { + static async isAccountExpired(): Promise { + const client = MatrixClientPeg.safeGet(); // todo(estelle) throws if client is not logged in. Is that what we want ? + const matrixId: string | null = client.credentials.userId; if (!matrixId) { - matrixId = MatrixClientPeg.getCredentials().userId; + // throw ? return ? todo(estelle) } try { - await MatrixClientPeg.get().getProfileInfo(matrixId); - } catch (err) { + await client.getProfileInfo(matrixId!); + } catch (error) { + const err = error as MatrixError; if (err.errcode === "ORG_MATRIX_EXPIRED_ACCOUNT") { return true; } From a9d226baeff60d1775a7842edbdae4b24e417ab9 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 16:27:22 +0200 Subject: [PATCH 02/15] Update more code --- src/tchap/util/TchapUtils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index 4c4a400d9..c698c1a25 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -157,8 +157,9 @@ export default class TchapUtils { * @returns true is a map tile server is present in config or wellknown. */ static isMapConfigured(): boolean { + const cli = MatrixClientPeg.get(); try { - findMapStyleUrl(); + findMapStyleUrl(cli); return true; } catch (e) { return false; @@ -189,6 +190,8 @@ export default class TchapUtils { const k = keys[i]; url += k + "=" + encodeURIComponent(params[k]); } + // todo(estelle) : write unit test, then try replacing by : + // url += "?" + new URLSearchParams(params).toString(); return url; } @@ -197,9 +200,10 @@ export default class TchapUtils { * @returns true if the mail was sent succesfully, false otherwise */ static async requestNewExpiredAccountEmail(): Promise { - console.log(":tchap: Requesting an email to renew to account"); - const homeserverUrl = MatrixClientPeg.get().getHomeserverUrl(); - const accessToken = MatrixClientPeg.get().getAccessToken(); + console.debug(":tchap: Requesting an email to renew to account"); // todo(estelle) reuse logger class + const client = MatrixClientPeg.get(); // todo(estelle) what to do if client is null ? use safeGet or get ? + const homeserverUrl = client.getHomeserverUrl(); + const accessToken = client.getAccessToken(); //const url = `${homeserverUrl}/_matrix/client/unstable/account_validity/send_mail`; const url = `${homeserverUrl}${TchapApi.accountValidityResendEmailUrl}`; const options = { From 6c5d6cb1d1e64242b789f0321cec76785c2f2162 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 16:39:11 +0200 Subject: [PATCH 03/15] Small cleanups and todos --- .../views/dialogs/ExpiredAccountDialog.tsx | 4 ++-- src/tchap/lib/ExpiredAccountHandler.ts | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index 4f76c6b13..b87744656 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -9,7 +9,7 @@ import TchapUtils from "../../../util/TchapUtils"; interface IProps { onFinished(): void; onRequestNewEmail(): Promise; - emailDelay?: number; //delay betwenn 2 emails in seconds, by default 30 + emailDelay?: number; //delay between 2 emails in seconds, by default 30 } interface IState { @@ -33,7 +33,7 @@ enum ProcessState { */ export default class ExpiredAccountDialog extends React.Component { - constructor(props) { + constructor(props: IProps) { super(props); this.state = { isAccountExpired: false, diff --git a/src/tchap/lib/ExpiredAccountHandler.ts b/src/tchap/lib/ExpiredAccountHandler.ts index a2cc27154..c03dc71a8 100644 --- a/src/tchap/lib/ExpiredAccountHandler.ts +++ b/src/tchap/lib/ExpiredAccountHandler.ts @@ -28,12 +28,13 @@ class ExpiredAccountHandler { } /** - * register the listener after the Matrix Client has been initialized but before it is started + * Register to listen to expired account event. + * Registration is done after the Matrix Client has been initialized but before it is started. */ public register() { const expiredRegistrationId = this.dispatcher.register((payload: ActionPayload) => { if (payload.action === "will_start_client") { - console.log(":tchap: register a listener for HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT events"); + console.log(":tchap: register a listener for HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT events"); // todo(estelle) logger const cli = MatrixClientPeg.get(); cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, this.boundOnExpiredAccountEvent); //unregister callback once the work is done @@ -46,14 +47,14 @@ class ExpiredAccountHandler { * When account expired account happens, display the panel if not open yet. */ private onExpiredAccountError() { - console.log(":tchap: Expired Account Error received"); + console.log(":tchap: Expired Account Error received"); // todo(estelle) reuse logger class if (this.isPanelOpen) { return; } //shutdown all matrix react services, but without unsetting the client stopMatrixClient(false); - console.log(":tchap: matrix react services have been shutdown"); + console.log(":tchap: matrix react services have been shutdown"); // todo(estelle) reuse logger class //should we sent the email directly? Normally they should have received already an email 7 days earlier this.showExpirationPanel(); @@ -63,7 +64,7 @@ class ExpiredAccountHandler { private async showExpirationPanel() { Modal.createDialog( ExpiredAccountDialog, - { + { /* props */ onRequestNewEmail: () => { return TchapUtils.requestNewExpiredAccountEmail(); }, @@ -74,10 +75,10 @@ class ExpiredAccountHandler { }, //todo: define which static/dynamic settings are needed for this dialog }, - null, - false, - true, - { + undefined /* className */, + false /* isPriorityModal */, + true /* isStaticModal */, + { /* options */ //close panel only if account is not expired onBeforeClose: async () => { //verify that the account is not expired anymore From c5bc61c090902c1c0868ba2d449facab5474c8e7 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 16:44:11 +0200 Subject: [PATCH 04/15] More light cleanup --- src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index b87744656..868288c65 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -27,10 +27,9 @@ enum ProcessState { ACCOUNT_RENEWED, } /** - * Expired Account is displayed when the user account is expired. It can not be cancel until the account is renewed. + * ExpiredAccountDialog is displayed when the user account is expired. It can not be canceled until the account is renewed. * This panel is exclusively opened by the listener ExpiredAccountHandler * This component is required when activating the plugin synapse-email-account-validity on the server side: https://github.com/matrix-org/synapse-email-account-validity - */ export default class ExpiredAccountDialog extends React.Component { constructor(props: IProps) { From edc83a14977d6b71b6cda11390a8ab0dbd8c4cee Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 17:03:41 +0200 Subject: [PATCH 05/15] Tiny translation fixes --- modules/tchap-translations/tchap_translations.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index 6240d3dff..93f87936d 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -301,7 +301,7 @@ "Verified!": { "en": "Verified!", "fr": "Vérifié !" }, "Wait for at least %(wait)s seconds between two emails": { "en": "Wait for at least %(wait)s seconds between two emails", - "fr": "Attendez au moins %(wait)s secondes entre l'envoi de deux mails" + "fr": "Attendez au moins %(wait)s secondes entre l'envoi de deux emails" }, "Warning: this is the only time this code will be displayed!": { "en": "Warning: this is the only time this code will be displayed!", @@ -322,8 +322,8 @@ "fr": "Vos Clés Tchap (clés de chiffrement) ont été sauvegardées avec succès dans le fichier tchap-keys.txt. Vous pourrez les importer à votre prochaine connexion pour déverrouiller vos messages (en savoir plus)." }, "Your account is still expired, please follow the link in the email you have received to renew it": { - "en": "Your account is still expired, please follow the link in the email you should have received to renew it", - "fr": "Votre compte est toujours expiré, merci de cliquer sur le lien reçu dans le mail de renouvellement" + "en": "Your account is still expired, please follow the link in the email you should have received to renew it.", + "fr": "Votre compte est toujours expiré, merci de cliquer sur le lien reçu dans le mail de renouvellement." }, "Your last three login attempts have failed. Please try again in a few minutes.": { "en": "Your last three login attempts have failed. Please try again in a few minutes.", From 4b693f77a73af9681f5ca02ee277c9369e9d9b00 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Tue, 9 Apr 2024 17:25:42 +0200 Subject: [PATCH 06/15] Use safeGet everywhere, so that it will throw if client is not initialised. --- src/tchap/lib/ExpiredAccountHandler.ts | 3 ++- src/tchap/util/TchapUtils.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/tchap/lib/ExpiredAccountHandler.ts b/src/tchap/lib/ExpiredAccountHandler.ts index c03dc71a8..0745c30b3 100644 --- a/src/tchap/lib/ExpiredAccountHandler.ts +++ b/src/tchap/lib/ExpiredAccountHandler.ts @@ -35,7 +35,8 @@ class ExpiredAccountHandler { const expiredRegistrationId = this.dispatcher.register((payload: ActionPayload) => { if (payload.action === "will_start_client") { console.log(":tchap: register a listener for HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT events"); // todo(estelle) logger - const cli = MatrixClientPeg.get(); + // safeGet will throw if client is not initialised yet. We don't handle it because we don't know when it would happen. + const cli = MatrixClientPeg.safeGet(); cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, this.boundOnExpiredAccountEvent); //unregister callback once the work is done this.dispatcher.unregister(expiredRegistrationId); diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index c698c1a25..8580eb3c4 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -18,9 +18,9 @@ export default class TchapUtils { * @returns {string} The shortened value of getDomain(). */ static getShortDomain(): string { - const cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.safeGet(); const baseDomain = cli.getDomain(); - const domain = baseDomain.split(".tchap.gouv.fr")[0].split(".").reverse().filter(Boolean)[0]; + const domain = baseDomain?.split(".tchap.gouv.fr")[0].split(".").reverse().filter(Boolean)[0]; return this.capitalize(domain) || "Tchap"; } @@ -34,7 +34,7 @@ export default class TchapUtils { showForumFederationSwitch: boolean; forumFederationSwitchDefaultValue?: boolean; } { - const cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.safeGet(); const baseDomain = cli.getDomain(); // Only show the federate switch to defense users : it's difficult to understand, so we avoid @@ -148,7 +148,7 @@ export default class TchapUtils { * @returns Promise is cross signing is supported by home server or false */ static async isCrossSigningSupportedByServer(): Promise { - const cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.safeGet(); return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); } @@ -157,7 +157,7 @@ export default class TchapUtils { * @returns true is a map tile server is present in config or wellknown. */ static isMapConfigured(): boolean { - const cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.safeGet(); try { findMapStyleUrl(cli); return true; @@ -201,7 +201,10 @@ export default class TchapUtils { */ static async requestNewExpiredAccountEmail(): Promise { console.debug(":tchap: Requesting an email to renew to account"); // todo(estelle) reuse logger class - const client = MatrixClientPeg.get(); // todo(estelle) what to do if client is null ? use safeGet or get ? + + // safeGet will throw if client is not initialised. We don't handle it because we don't know when this would happen. + const client = MatrixClientPeg.safeGet(); + const homeserverUrl = client.getHomeserverUrl(); const accessToken = client.getAccessToken(); //const url = `${homeserverUrl}/_matrix/client/unstable/account_validity/send_mail`; @@ -230,7 +233,7 @@ export default class TchapUtils { * @returns true if account is expired, false otherwise */ static async isAccountExpired(): Promise { - const client = MatrixClientPeg.safeGet(); // todo(estelle) throws if client is not logged in. Is that what we want ? + const client = MatrixClientPeg.safeGet(); const matrixId: string | null = client.credentials.userId; if (!matrixId) { // throw ? return ? todo(estelle) From 5089ffcfb3b3b89aad6ac15d6265f75af62380a5 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 14:00:53 +0200 Subject: [PATCH 07/15] Add a wait spinner when sending email --- .../views/dialogs/ExpiredAccountDialog.tsx | 21 +++++++++++++------ src/tchap/util/TchapUtils.ts | 3 ++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index 868288c65..53dccd7fa 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -3,23 +3,26 @@ import React from "react"; import { _t } from "matrix-react-sdk/src/languageHandler"; import BaseDialog from "matrix-react-sdk/src/components/views/dialogs/BaseDialog"; import DialogButtons from "matrix-react-sdk/src/components/views/elements/DialogButtons"; +import InlineSpinner from "matrix-react-sdk/src/components/views/elements/InlineSpinner"; import TchapUtils from "../../../util/TchapUtils"; interface IProps { onFinished(): void; onRequestNewEmail(): Promise; - emailDelay?: number; //delay between 2 emails in seconds, by default 30 + emailDelaySecs?: number; //delay between 2 emails in seconds, by default 30 } interface IState { - emailDelay: number; //delay betwenn 2 emails in seconds, by default 30 + emailDelaySecs: number; //delay betwenn 2 emails in seconds, by default 30 isAccountExpired: boolean; //todo: not used yet newEmailSentTimestamp: number; //timestamp ProcessState: ProcessState; } enum ProcessState { + START, + SENDING_EMAIL, EMAIL_MUST_WAIT, EMAIL_SUCCESS, EMAIL_FAILURE, @@ -37,8 +40,8 @@ export default class ExpiredAccountDialog extends React.Component { this.setState({ newEmailSentTimestamp: success ? Date.now() : this.state.newEmailSentTimestamp, @@ -94,12 +100,15 @@ export default class ExpiredAccountDialog extends React.Component // todo translation+format or spinner + break; case ProcessState.EMAIL_MUST_WAIT: //don't know which class should decorate this message, it is not really an error //its goal is to avoid users to click twice or more on the button and spam themselves alertMessage = (

- {_t("Wait for at least %(wait)s seconds between two emails", { wait: this.state.emailDelay })} + {_t("Wait for at least %(wait)s seconds between two emails", { wait: this.state.emailDelaySecs })}

); break; diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index 8580eb3c4..9e2e571fe 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -236,7 +236,8 @@ export default class TchapUtils { const client = MatrixClientPeg.safeGet(); const matrixId: string | null = client.credentials.userId; if (!matrixId) { - // throw ? return ? todo(estelle) + // user is not logged in. Or something went wrong. + return false; } try { await client.getProfileInfo(matrixId!); From 2528da17ae4156db193e18f747fcbbbb0a46ed0e Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 14:16:31 +0200 Subject: [PATCH 08/15] User proper logger --- src/tchap/lib/ExpiredAccountHandler.ts | 7 ++++--- src/tchap/util/TchapUtils.ts | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/tchap/lib/ExpiredAccountHandler.ts b/src/tchap/lib/ExpiredAccountHandler.ts index 0745c30b3..2ec64e454 100644 --- a/src/tchap/lib/ExpiredAccountHandler.ts +++ b/src/tchap/lib/ExpiredAccountHandler.ts @@ -5,6 +5,7 @@ import { stopMatrixClient } from "matrix-react-sdk/src/Lifecycle"; import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg"; import Modal from "matrix-react-sdk/src/Modal"; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg"; +import { logger } from "matrix-js-sdk/src/logger"; import ExpiredAccountDialog from "../components/views/dialogs/ExpiredAccountDialog"; import TchapUtils from "../util/TchapUtils"; @@ -34,7 +35,7 @@ class ExpiredAccountHandler { public register() { const expiredRegistrationId = this.dispatcher.register((payload: ActionPayload) => { if (payload.action === "will_start_client") { - console.log(":tchap: register a listener for HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT events"); // todo(estelle) logger + logger.debug(":tchap: register a listener for HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT events"); // safeGet will throw if client is not initialised yet. We don't handle it because we don't know when it would happen. const cli = MatrixClientPeg.safeGet(); cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, this.boundOnExpiredAccountEvent); @@ -48,14 +49,14 @@ class ExpiredAccountHandler { * When account expired account happens, display the panel if not open yet. */ private onExpiredAccountError() { - console.log(":tchap: Expired Account Error received"); // todo(estelle) reuse logger class + logger.debug(":tchap: Expired Account Error received"); if (this.isPanelOpen) { return; } //shutdown all matrix react services, but without unsetting the client stopMatrixClient(false); - console.log(":tchap: matrix react services have been shutdown"); // todo(estelle) reuse logger class + logger.debug(":tchap: matrix react services have been shutdown"); //should we sent the email directly? Normally they should have received already an email 7 days earlier this.showExpirationPanel(); diff --git a/src/tchap/util/TchapUtils.ts b/src/tchap/util/TchapUtils.ts index 9e2e571fe..4585124d8 100644 --- a/src/tchap/util/TchapUtils.ts +++ b/src/tchap/util/TchapUtils.ts @@ -3,6 +3,7 @@ import SdkConfig from "matrix-react-sdk/src/SdkConfig"; import AutoDiscoveryUtils from "matrix-react-sdk/src/utils/AutoDiscoveryUtils"; import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig"; import { findMapStyleUrl } from "matrix-react-sdk/src/utils/location"; +import { logger } from "matrix-js-sdk/src/logger"; import TchapApi from "./TchapApi"; import { ClientConfig } from "~tchap-web/yarn-linked-dependencies/matrix-js-sdk/src/autodiscovery"; @@ -98,7 +99,7 @@ export default class TchapUtils { }; }) .catch((error) => { - console.error("Could not find homeserver for this email", error); + logger.error("Could not find homeserver for this email", error); return; }); }; @@ -200,7 +201,7 @@ export default class TchapUtils { * @returns true if the mail was sent succesfully, false otherwise */ static async requestNewExpiredAccountEmail(): Promise { - console.debug(":tchap: Requesting an email to renew to account"); // todo(estelle) reuse logger class + logger.debug(":tchap: Requesting an email to renew to account"); // safeGet will throw if client is not initialised. We don't handle it because we don't know when this would happen. const client = MatrixClientPeg.safeGet(); @@ -218,11 +219,11 @@ export default class TchapUtils { return fetch(url, options) .then((response) => { - console.log(":tchap: email NewExpiredAccountEmail sent", response); + logger.debug(":tchap: email NewExpiredAccountEmail sent", response); return true; }) .catch((err) => { - console.error(":tchap: email NewExpiredAccountEmail error", err); + logger.error(":tchap: email NewExpiredAccountEmail error", err); return false; }); } @@ -257,7 +258,7 @@ export default class TchapUtils { */ static async isCurrentlyUsingBluetooth(): Promise { if (!navigator.mediaDevices?.enumerateDevices) { - console.log("enumerateDevices() not supported. Cannot know if there is a bluetooth device."); + logger.warn("enumerateDevices() not supported. Cannot know if there is a bluetooth device."); return false; } else { // List cameras and microphones. @@ -266,7 +267,7 @@ export default class TchapUtils { .then((devices) => { let hasBluetooth = false; devices.forEach((device) => { - console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`); + logger.debug(`${device.kind}: ${device.label} id = ${device.deviceId}`); if (device.kind === "audioinput") { if (device.label.toLowerCase().includes("bluetooth")) { hasBluetooth = true; @@ -276,7 +277,7 @@ export default class TchapUtils { return hasBluetooth; }) .catch((err) => { - console.error(`${err.name}: ${err.message}`); + logger.error(`${err.name}: ${err.message}`); return false; }); } From 15e2fb98de35c06166c2f9b9e5bb6f76595e3f09 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 14:30:03 +0200 Subject: [PATCH 09/15] Test drafts --- .../components/views/dialogs/ExpiredAccountDialog.tsx | 2 +- .../views/dialogs/ExpiredAccountDialog-test.tsx | 5 +++++ test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx create mode 100644 test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index 53dccd7fa..a71257092 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -101,7 +101,7 @@ export default class ExpiredAccountDialog extends React.Component // todo translation+format or spinner + alertMessage = break; case ProcessState.EMAIL_MUST_WAIT: //don't know which class should decorate this message, it is not really an error diff --git a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx new file mode 100644 index 000000000..3bdec6338 --- /dev/null +++ b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx @@ -0,0 +1,5 @@ +describe("ExpiredAccountDialog", () => { + it("sends email when 'Send email' button is clicked", () => {}); + it("when 'I renewed' is clicked, and the account is renewed, it displays 'cool' and unblocks user (todo)", () => {}); + it("when 'I renewed' is clicked, and the account is not renewed, it displays 'not cool' and keeps dialog up (todo)", () => {}); +}); diff --git a/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts new file mode 100644 index 000000000..4d18fa82f --- /dev/null +++ b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts @@ -0,0 +1,9 @@ +describe("ExpiredAccountHandler", () => { + it("displays dialog when account is expired", () => { + // Modal.createDialog + // create handler + // dispatch action : will_start_client + // cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT) + // expect Modal.createDialog to have been called, with ExpiredAccountDialog class + }); +}); From 105ba3f0263e40d84a9ae27c5ea5de39875c2531 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 15:00:09 +0200 Subject: [PATCH 10/15] Implment ExpiredAccountHandler-test --- .../tchap/lib/ExpiredAccountHandler-test.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts index 4d18fa82f..f1c763608 100644 --- a/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts +++ b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts @@ -1,9 +1,52 @@ +import { mocked, MockedObject } from "jest-mock"; + +import ExpiredAccountDialog from "~tchap-web/src/tchap/components/views/dialogs/ExpiredAccountDialog"; +import ExpiredAccountHandler from "~tchap-web/src/tchap/lib/ExpiredAccountHandler"; +import { HttpApiEvent, MatrixClient } from "~tchap-web/yarn-linked-dependencies/matrix-js-sdk/src/matrix"; +import Modal from "~tchap-web/yarn-linked-dependencies/matrix-react-sdk/src/Modal"; +import defaultDispatcher from "~tchap-web/yarn-linked-dependencies/matrix-react-sdk/src/dispatcher/dispatcher"; +import { getMockClientWithEventEmitter } from "~tchap-web/yarn-linked-dependencies/matrix-react-sdk/test/test-utils"; + describe("ExpiredAccountHandler", () => { + let mockClient: MockedObject; + + beforeEach(() => { + Modal.createDialog = jest.fn(); + // @ts-ignore mock (type error because empty return) + mocked(Modal.createDialog).mockReturnValue({}); + + mockClient = getMockClientWithEventEmitter({ + stopClient: jest.fn(), + removeAllListeners: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it("displays dialog when account is expired", () => { - // Modal.createDialog - // create handler - // dispatch action : will_start_client + // handler instance is created when import ExpiredAccountHandler is run. + ExpiredAccountHandler.register(); + + // Simulate start of app, for handler to initialise + defaultDispatcher.dispatch({ action: "will_start_client" }, true); + + // Simulate expired error // cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT) - // expect Modal.createDialog to have been called, with ExpiredAccountDialog class + // this.eventEmitter.emit(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, err); + mockClient.emit(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT); + + expect(Modal.createDialog).toHaveBeenCalledWith( + ExpiredAccountDialog, + { + onRequestNewEmail: expect.any(Function), + onFinished: expect.any(Function), + }, + undefined /* className */, + false /* isPriorityModal */, + true /* isStaticModal */, + { onBeforeClose: expect.any(Function) }, + ); }); }); From afd702645e5ad73e9508f4a91b54b6d7cafe3cf9 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 15:42:24 +0200 Subject: [PATCH 11/15] More tests for dialog --- .../views/dialogs/ExpiredAccountDialog.tsx | 8 +-- .../dialogs/ExpiredAccountDialog-test.tsx | 62 ++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index a71257092..ad174e7f2 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -96,7 +96,7 @@ export default class ExpiredAccountDialog extends React.Component{_t("An email has been sent to you. Click on the link it contains, click below.")}

); let alertMessage = null; - let requestNewEmailButton = ; + let requestNewEmailButton = ; let okButtonText = _t("I renewed the validity of my account"); switch (this.state.ProcessState) { @@ -107,18 +107,18 @@ export default class ExpiredAccountDialog extends React.Component +

{_t("Wait for at least %(wait)s seconds between two emails", { wait: this.state.emailDelaySecs })}

); break; case ProcessState.EMAIL_FAILURE: alertMessage = ( -

{_t("The email was not sent sucessfully, please retry in a moment")}

+

{_t("The email was not sent sucessfully, please retry in a moment")}

); break; case ProcessState.EMAIL_SUCCESS: - alertMessage =

{_t("A new email has been sent")}

; + alertMessage =

{_t("A new email has been sent")}

; break; case ProcessState.ACCOUNT_STILL_EXPIRED: alertMessage = ( diff --git a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx index 3bdec6338..ed592d46a 100644 --- a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx +++ b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx @@ -1,5 +1,65 @@ +import * as React from "react"; +import { render, screen } from "@testing-library/react"; + +import ExpiredAccountDialog from "~tchap-web/src/tchap/components/views/dialogs/ExpiredAccountDialog"; +import { flushPromises } from "~tchap-web/yarn-linked-dependencies/matrix-react-sdk/test/test-utils"; + describe("ExpiredAccountDialog", () => { - it("sends email when 'Send email' button is clicked", () => {}); + describe("'Send email' button", () => { + it("sends email when button is clicked", async () => { + // true = email sent successfully + const onRequestNewEmailMock = jest.fn().mockResolvedValue(true); + + const component = render( + , + ); + const sendEmailButton = screen.getByTestId("dialog-send-email-button"); + sendEmailButton.click(); + + expect(onRequestNewEmailMock).toHaveBeenCalled(); + // wait spinner is displayed + expect(component.container.querySelector(".mx_InlineSpinner")).toBeTruthy(); + + await flushPromises(); + // confirmation message is displayed + screen.getByTestId("dialog-email-sent-message"); + }); + + it("tells user if sending email failed", async () => { + // false = email sending failed + const onRequestNewEmailMock = jest.fn().mockResolvedValue(false); + + const component = render( + , + ); + const sendEmailButton = screen.getByTestId("dialog-send-email-button"); + sendEmailButton.click(); + + expect(onRequestNewEmailMock).toHaveBeenCalled(); + // wait spinner is displayed + expect(component.container.querySelector(".mx_InlineSpinner")).toBeTruthy(); + + await flushPromises(); + // confirmation message is displayed + screen.getByTestId("dialog-email-failure-message"); + }); + + it("sends only one email when button is clicked twice", async () => { + // true = email sent successfully + const onRequestNewEmailMock = jest.fn().mockResolvedValue(true); + + render(); + const sendEmailButton = screen.getByTestId("dialog-send-email-button"); + + sendEmailButton.click(); + await flushPromises(); + sendEmailButton.click(); + await flushPromises(); + expect(onRequestNewEmailMock).toHaveBeenCalledTimes(1); // only 1 email sent + screen.getByTestId("dialog-email-wait-message"); // user notified that they must wait + }); + }); + it("when 'I renewed' is clicked, and the account is renewed, it displays 'cool' and unblocks user (todo)", () => {}); it("when 'I renewed' is clicked, and the account is not renewed, it displays 'not cool' and keeps dialog up (todo)", () => {}); }); From d9d6fdef98d2e0458996c87abecde0735521ab71 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 16:03:56 +0200 Subject: [PATCH 12/15] I renewed tests --- .../tchap_translations.json | 4 +- .../views/dialogs/ExpiredAccountDialog.tsx | 6 +-- .../dialogs/ExpiredAccountDialog-test.tsx | 43 ++++++++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index 93f87936d..98416f9e0 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -210,7 +210,7 @@ "Read the Privacy Policy": { "en": "Read the Privacy Policy", "fr": "Lire la Politique de Confidentialité" }, "invite|recents_section": { "en": "Recent direct messages", "fr": "Messages directs récents" }, "invite|suggestions_section": { "en": "Recent direct messages", "fr": "Messages directs récents" }, - "Reload the app": { "en": "Reload page", "fr": "Rafraîchir la page" }, + "Reload page": { "en": "Reload page", "fr": "Rafraîchir la page" }, "Request a renewal email": { "en": "Request a renewal email", "fr": "Demander l’envoi d’un nouvel email" }, "encryption|access_secret_storage_dialog|restoring": { "en": "Restoring messages from backup", @@ -247,7 +247,7 @@ "en": "Your email adress is not allowed on Tchap. Make an autorization request here", "fr": "Votre adresse mail n'est pas autorisée sur Tchap. Demandez l'accès à Tchap pour votre administration via ce formulaire" }, - "The app will reload now": { + "You can refresh the page to continue your conversations": { "en": "You can refresh the page to continue your conversations", "fr": "Vous pouvez dorénavant rafraîchir la page pour continuer vos conversations" }, diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index ad174e7f2..2edf30956 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -122,7 +122,7 @@ export default class ExpiredAccountDialog extends React.Component +

{_t( "Your account is still expired, please follow the link in the email you have received to renew it", )} @@ -131,8 +131,8 @@ export default class ExpiredAccountDialog extends React.Component{_t("The app will reload now")}

; - okButtonText = _t("Reload the app"); + descriptionMessage =

{_t("You can refresh the page to continue your conversations")}

; + okButtonText = _t("Reload page"); alertMessage = null; requestNewEmailButton = null; break; diff --git a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx index ed592d46a..316cf9f31 100644 --- a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx +++ b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx @@ -3,8 +3,13 @@ import { render, screen } from "@testing-library/react"; import ExpiredAccountDialog from "~tchap-web/src/tchap/components/views/dialogs/ExpiredAccountDialog"; import { flushPromises } from "~tchap-web/yarn-linked-dependencies/matrix-react-sdk/test/test-utils"; +import TchapUtils from "~tchap-web/src/tchap/util/TchapUtils"; describe("ExpiredAccountDialog", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + describe("'Send email' button", () => { it("sends email when button is clicked", async () => { // true = email sent successfully @@ -60,6 +65,40 @@ describe("ExpiredAccountDialog", () => { }); }); - it("when 'I renewed' is clicked, and the account is renewed, it displays 'cool' and unblocks user (todo)", () => {}); - it("when 'I renewed' is clicked, and the account is not renewed, it displays 'not cool' and keeps dialog up (todo)", () => {}); + describe("'I renewed my account' button", () => { + it("if the account is renewed, it displays success message and ... ? (todo)", async () => { + const onFinishedMock = jest.fn(); + jest.spyOn(TchapUtils, "isAccountExpired").mockResolvedValue(false); + + const component = render( + , + ); + const iRenewedButton = component.getByTestId("dialog-primary-button"); + + iRenewedButton.click(); + await flushPromises(); + + // display happy message todo + // "votre compte a été renouvelé" + // primary label is "Rafraîchir la page" + // click primary : onFinished + }); + + it("if the account is still not renewed, it displays explanation and keeps dialog up", async () => { + const onFinishedMock = jest.fn(); + jest.spyOn(TchapUtils, "isAccountExpired").mockResolvedValue(true); + + const component = render( + , + ); + const iRenewedButton = component.getByTestId("dialog-primary-button"); + + iRenewedButton.click(); + await flushPromises(); + + // display unhappy message + screen.getByTestId("dialog-account-still-expired-message"); + expect(onFinishedMock).not.toHaveBeenCalled(); + }); + }); }); From 3588e1d25e899d15fbe5aa03288f91c37425b5db Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 16:48:44 +0200 Subject: [PATCH 13/15] One more test --- .../dialogs/ExpiredAccountDialog-test.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx index 316cf9f31..3b6d71022 100644 --- a/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx +++ b/test/unit-tests/tchap/components/views/dialogs/ExpiredAccountDialog-test.tsx @@ -66,22 +66,27 @@ describe("ExpiredAccountDialog", () => { }); describe("'I renewed my account' button", () => { - it("if the account is renewed, it displays success message and ... ? (todo)", async () => { + it("if the account is renewed, it displays confirmation message, and offers page reload", async () => { const onFinishedMock = jest.fn(); jest.spyOn(TchapUtils, "isAccountExpired").mockResolvedValue(false); const component = render( , ); - const iRenewedButton = component.getByTestId("dialog-primary-button"); + const primaryButton = component.getByTestId("dialog-primary-button"); - iRenewedButton.click(); + // click "I renewed my account" + expect(primaryButton).toHaveTextContent("I renewed"); + primaryButton.click(); await flushPromises(); - // display happy message todo - // "votre compte a été renouvelé" - // primary label is "Rafraîchir la page" - // click primary : onFinished + // Account renewed ! Display confirmation + component.getByText("your account has been renewed", { exact: false }); + expect(primaryButton).toHaveTextContent("Reload page"); // new button text + + // click "Reload page" + primaryButton.click(); + expect(onFinishedMock).toHaveBeenCalled(); // onFinish will do the page reload. The actual reload is out of scope here. }); it("if the account is still not renewed, it displays explanation and keeps dialog up", async () => { From bee751b6afa0c98a408a15e744d32fb65a63e8f5 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 17:02:20 +0200 Subject: [PATCH 14/15] Remove old comment --- test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts index f1c763608..9e5cf110c 100644 --- a/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts +++ b/test/unit-tests/tchap/lib/ExpiredAccountHandler-test.ts @@ -25,16 +25,16 @@ describe("ExpiredAccountHandler", () => { jest.clearAllMocks(); }); + // TODO : it would be nice to test that our handler is propoerly registered on app load, in src/vector/index.ts + // Gonna require a lot of mocking... it("displays dialog when account is expired", () => { - // handler instance is created when import ExpiredAccountHandler is run. + // handler instance is created when "import ExpiredAccountHandler" is run. ExpiredAccountHandler.register(); // Simulate start of app, for handler to initialise defaultDispatcher.dispatch({ action: "will_start_client" }, true); // Simulate expired error - // cli.on(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT) - // this.eventEmitter.emit(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT, err); mockClient.emit(HttpApiEvent.ORG_MATRIX_EXPIRED_ACCOUNT); expect(Modal.createDialog).toHaveBeenCalledWith( From 7b42a72d18f8a9d96895d5ea6afc8c3e388cdf2e Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Apr 2024 17:17:23 +0200 Subject: [PATCH 15/15] Last cleanup --- src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx | 2 -- src/tchap/util/TchapUtils.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx index 2edf30956..4418e43b7 100644 --- a/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx +++ b/src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx @@ -15,7 +15,6 @@ interface IProps { interface IState { emailDelaySecs: number; //delay betwenn 2 emails in seconds, by default 30 - isAccountExpired: boolean; //todo: not used yet newEmailSentTimestamp: number; //timestamp ProcessState: ProcessState; } @@ -38,7 +37,6 @@ export default class ExpiredAccountDialog extends React.Component