Skip to content

Commit

Permalink
Merge pull request #971 from tchapgouv/959-update-account-expiration
Browse files Browse the repository at this point in the history
Update account expiration UI code
  • Loading branch information
estellecomment authored Apr 16, 2024
2 parents ecb01bd + 7b42a72 commit dd2c9d0
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 56 deletions.
10 changes: 5 additions & 5 deletions modules/tchap-translations/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -247,7 +247,7 @@
"en": "Your email adress is not allowed on Tchap. Make an autorization request <a>here</a>",
"fr": "Votre adresse mail n'est pas autorisée sur Tchap. Demandez l'accès à Tchap pour votre administration via <a>ce formulaire</a>"
},
"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"
},
Expand Down Expand Up @@ -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!",
Expand All @@ -322,8 +322,8 @@
"fr": "Vos Clés Tchap (clés de chiffrement) ont été sauvegardées avec succès dans le fichier <b>tchap-keys.txt</b>. Vous pourrez les importer à votre prochaine connexion pour déverrouiller vos messages (<a>en savoir plus</a>)."
},
"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.",
Expand Down
42 changes: 24 additions & 18 deletions src/tchap/components/views/dialogs/ExpiredAccountDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,51 @@ 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<any>;
emailDelay?: number; //delay betwenn 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
isAccountExpired: boolean; //todo: not used yet
emailDelaySecs: number; //delay betwenn 2 emails in seconds, by default 30
newEmailSentTimestamp: number; //timestamp
ProcessState: ProcessState;
}

enum ProcessState {
START,
SENDING_EMAIL,
EMAIL_MUST_WAIT,
EMAIL_SUCCESS,
EMAIL_FAILURE,
ACCOUNT_STILL_EXPIRED,
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<IProps, IState> {
constructor(props) {
constructor(props: IProps) {
super(props);
this.state = {
isAccountExpired: false,
newEmailSentTimestamp: 0,
emailDelay: this.props.emailDelay || 30, //seconds
ProcessState: null,
emailDelaySecs: this.props.emailDelaySecs || 30,
ProcessState: ProcessState.START,
};
}

//check if an email can be sent of must wait a bit
private mustWait() {
return (
this.state.newEmailSentTimestamp != 0 &&
Date.now() - this.state.newEmailSentTimestamp < this.state.emailDelay * 1000
Date.now() - this.state.newEmailSentTimestamp < this.state.emailDelaySecs * 1000
);
}

Expand Down Expand Up @@ -77,6 +77,9 @@ export default class ExpiredAccountDialog extends React.Component<IProps, IState
}

//send the new email requested
this.setState({
ProcessState: ProcessState.SENDING_EMAIL,
})
this.props.onRequestNewEmail().then((success) => {
this.setState({
newEmailSentTimestamp: success ? Date.now() : this.state.newEmailSentTimestamp,
Expand All @@ -91,30 +94,33 @@ export default class ExpiredAccountDialog extends React.Component<IProps, IState
<p>{_t("An email has been sent to you. Click on the link it contains, click below.")}</p>
);
let alertMessage = null;
let requestNewEmailButton = <button onClick={this.onRequestEmail}>{_t("Request a renewal email")}</button>;
let requestNewEmailButton = <button onClick={this.onRequestEmail} data-testid="dialog-send-email-button">{_t("Request a renewal email")}</button>;
let okButtonText = _t("I renewed the validity of my account");

switch (this.state.ProcessState) {
case ProcessState.SENDING_EMAIL:
alertMessage = <InlineSpinner />
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 = (
<p className="">
{_t("Wait for at least %(wait)s seconds between two emails", { wait: this.state.emailDelay })}
<p className="" data-testid="dialog-email-wait-message">
{_t("Wait for at least %(wait)s seconds between two emails", { wait: this.state.emailDelaySecs })}
</p>
);
break;
case ProcessState.EMAIL_FAILURE:
alertMessage = (
<p className="text-error">{_t("The email was not sent sucessfully, please retry in a moment")}</p>
<p className="text-error" data-testid="dialog-email-failure-message">{_t("The email was not sent sucessfully, please retry in a moment")}</p>
);
break;
case ProcessState.EMAIL_SUCCESS:
alertMessage = <p className="text-success">{_t("A new email has been sent")}</p>;
alertMessage = <p className="text-success" data-testid="dialog-email-sent-message">{_t("A new email has been sent")}</p>;
break;
case ProcessState.ACCOUNT_STILL_EXPIRED:
alertMessage = (
<p className="text-error">
<p className="text-error" data-testid="dialog-account-still-expired-message">
{_t(
"Your account is still expired, please follow the link in the email you have received to renew it",
)}
Expand All @@ -123,8 +129,8 @@ export default class ExpiredAccountDialog extends React.Component<IProps, IState
break;
case ProcessState.ACCOUNT_RENEWED:
titleMessage = _t("Congratulations, your account has been renewed");
descriptionMessage = <p>{_t("The app will reload now")}</p>;
okButtonText = _t("Reload the app");
descriptionMessage = <p>{_t("You can refresh the page to continue your conversations")}</p>;
okButtonText = _t("Reload page");
alertMessage = null;
requestNewEmailButton = null;
break;
Expand Down
23 changes: 13 additions & 10 deletions src/tchap/lib/ExpiredAccountHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,13 +29,15 @@ 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");
const cli = MatrixClientPeg.get();
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);
//unregister callback once the work is done
this.dispatcher.unregister(expiredRegistrationId);
Expand All @@ -46,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");
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");
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();
Expand All @@ -63,7 +66,7 @@ class ExpiredAccountHandler {
private async showExpirationPanel() {
Modal.createDialog(
ExpiredAccountDialog,
{
{ /* props */
onRequestNewEmail: () => {
return TchapUtils.requestNewExpiredAccountEmail();
},
Expand All @@ -74,10 +77,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
Expand Down
56 changes: 33 additions & 23 deletions src/tchap/util/TchapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ 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";
import { MatrixError } from "~tchap-web/yarn-linked-dependencies/matrix-js-sdk/src/http-api";

/**
* Tchap utils.
Expand All @@ -17,9 +19,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";
}
Expand All @@ -33,7 +35,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
Expand Down Expand Up @@ -97,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;
});
};
Expand Down Expand Up @@ -147,7 +149,7 @@ export default class TchapUtils {
* @returns Promise<true> is cross signing is supported by home server or false
*/
static async isCrossSigningSupportedByServer(): Promise<boolean> {
const cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.safeGet();
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
}

Expand All @@ -156,8 +158,9 @@ export default class TchapUtils {
* @returns true is a map tile server is present in config or wellknown.
*/
static isMapConfigured(): boolean {
const cli = MatrixClientPeg.safeGet();
try {
findMapStyleUrl();
findMapStyleUrl(cli);
return true;
} catch (e) {
return false;
Expand Down Expand Up @@ -188,6 +191,8 @@ export default class TchapUtils {
const k = keys[i];
url += k + "=" + encodeURIComponent(params[k]);
}
// todo : write unit test, then try replacing by :
// url += "?" + new URLSearchParams(params).toString();
return url;
}

Expand All @@ -196,10 +201,13 @@ export default class TchapUtils {
* @returns true if the mail was sent succesfully, false otherwise
*/
static async requestNewExpiredAccountEmail(): Promise<boolean> {
console.log(":tchap: Requesting an email to renew to account");
const homeserverUrl = MatrixClientPeg.get().getHomeserverUrl();
const accessToken = MatrixClientPeg.get().getAccessToken();
//const url = `${homeserverUrl}/_matrix/client/unstable/account_validity/send_mail`;
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();

const homeserverUrl = client.getHomeserverUrl();
const accessToken = client.getAccessToken();
const url = `${homeserverUrl}${TchapApi.accountValidityResendEmailUrl}`;
const options = {
method: "POST",
Expand All @@ -210,29 +218,31 @@ 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;
});
}

/**
* 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<boolean> {
static async isAccountExpired(): Promise<boolean> {
const client = MatrixClientPeg.safeGet();
const matrixId: string | null = client.credentials.userId;
if (!matrixId) {
matrixId = MatrixClientPeg.getCredentials().userId;
// user is not logged in. Or something went wrong.
return false;
}
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;
}
Expand All @@ -247,7 +257,7 @@ export default class TchapUtils {
*/
static async isCurrentlyUsingBluetooth(): Promise<boolean> {
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.
Expand All @@ -256,7 +266,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;
Expand All @@ -266,7 +276,7 @@ export default class TchapUtils {
return hasBluetooth;
})
.catch((err) => {
console.error(`${err.name}: ${err.message}`);
logger.error(`${err.name}: ${err.message}`);
return false;
});
}
Expand Down
Loading

0 comments on commit dd2c9d0

Please sign in to comment.