Merge branch 'staging' into preprod

This commit is contained in:
Maxime Lalo 2023-12-05 11:27:12 +01:00
commit 16576686fa
30 changed files with 1331 additions and 369 deletions

View File

@ -8,8 +8,69 @@ jobs:
build-push-docker-image: build-push-docker-image:
docker: docker:
- image: cimg/base:stable - image: cimg/base:stable
parameters:
NEXT_PUBLIC_BACK_API_HOST:
type: string
default: ""
NEXT_PUBLIC_BACK_API_PROTOCOL:
type: string
default: ""
NEXT_PUBLIC_BACK_API_ROOT_URL:
type: string
default: ""
NEXT_PUBLIC_BACK_API_VERSION:
type: string
default: ""
NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT:
type: string
default: ""
NEXT_PUBLIC_FC_CLIENT_ID:
type: string
default: ""
NEXT_PUBLIC_FRONT_APP_HOST:
type: string
default: ""
NEXT_PUBLIC_FRONT_APP_PORT:
type: string
default: ""
NEXT_PUBLIC_IDNOT_BASE_URL:
type: string
default: ""
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT:
type: string
default: ""
NEXT_PUBLIC_IDNOT_CLIENT_ID:
type: string
default: ""
NEXT_PUBLIC_DOCAPOST_API_URL:
type: string
default: ""
NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID:
type: string
default: ""
NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID:
type: string
default: ""
NEXT_PUBLIC_DOCAPOST_APP_ID:
type: string
default: ""
environment: environment:
TAG: << pipeline.git.tag >> TAG: << pipeline.git.tag >>
NEXT_PUBLIC_BACK_API_HOST: << parameters.NEXT_PUBLIC_BACK_API_HOST >>
NEXT_PUBLIC_BACK_API_PROTOCOL: << parameters.NEXT_PUBLIC_BACK_API_PROTOCOL >>
NEXT_PUBLIC_BACK_API_ROOT_URL: << parameters.NEXT_PUBLIC_BACK_API_ROOT_URL >>
NEXT_PUBLIC_BACK_API_VERSION: << parameters.NEXT_PUBLIC_BACK_API_VERSION >>
NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT: << parameters.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT >>
NEXT_PUBLIC_FC_CLIENT_ID: << parameters.NEXT_PUBLIC_FC_CLIENT_ID >>
NEXT_PUBLIC_FRONT_APP_HOST: << parameters.NEXT_PUBLIC_FRONT_APP_HOST >>
NEXT_PUBLIC_FRONT_APP_PORT: << parameters.NEXT_PUBLIC_FRONT_APP_PORT >>
NEXT_PUBLIC_IDNOT_BASE_URL: << parameters.NEXT_PUBLIC_IDNOT_BASE_URL >>
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: << parameters.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT >>
NEXT_PUBLIC_IDNOT_CLIENT_ID: << parameters.NEXT_PUBLIC_IDNOT_CLIENT_ID >>
NEXT_PUBLIC_DOCAPOST_API_URL: << parameters.NEXT_PUBLIC_DOCAPOST_API_URL >>
NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID: << parameters.NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID >>
NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID: << parameters.NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID >>
NEXT_PUBLIC_DOCAPOST_APP_ID: << parameters.NEXT_PUBLIC_DOCAPOST_APP_ID >>
steps: steps:
- checkout - checkout
- add_ssh_keys: - add_ssh_keys:
@ -52,6 +113,21 @@ workflows:
build-and-register-stg: build-and-register-stg:
jobs: jobs:
- build-push-docker-image: - build-push-docker-image:
NEXT_PUBLIC_BACK_API_HOST: api.stg.lecoffre.smart-chain.fr
NEXT_PUBLIC_BACK_API_PROTOCOL: https://
NEXT_PUBLIC_BACK_API_ROOT_URL: /api
NEXT_PUBLIC_BACK_API_VERSION: /v1
NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT: https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize
NEXT_PUBLIC_FC_CLIENT_ID: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e
NEXT_PUBLIC_FRONT_APP_HOST: https://app.stg.lecoffre.smart-chain.fr
NEXT_PUBLIC_FRONT_APP_PORT: "3000"
NEXT_PUBLIC_IDNOT_BASE_URL: "https://qual-connexion.idnot.fr"
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: "/IdPOAuth2/authorize/idnot_idp_v1"
NEXT_PUBLIC_IDNOT_CLIENT_ID: "4501646203F3EF67"
NEXT_PUBLIC_DOCAPOST_API_URL: "https://preprod.id360docaposte.com/api/1.0.0"
NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID: 49508376-b160-475d-9224-9bb6511215b8
NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID: 54c14875-f864-4819-8cd4-4fc6dd4a947b
NEXT_PUBLIC_DOCAPOST_APP_ID: leCoffre@smart_chain
context: context:
- sc-shared-prd - sc-shared-prd
filters: filters:
@ -70,6 +146,21 @@ workflows:
build-and-register-ppd: build-and-register-ppd:
jobs: jobs:
- build-push-docker-image: - build-push-docker-image:
NEXT_PUBLIC_BACK_API_HOST: api.ppd.lecoffre.smart-chain.fr
NEXT_PUBLIC_BACK_API_PROTOCOL: https://
NEXT_PUBLIC_BACK_API_ROOT_URL: /api
NEXT_PUBLIC_BACK_API_VERSION: /v1
NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT: https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize
NEXT_PUBLIC_FC_CLIENT_ID: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e
NEXT_PUBLIC_FRONT_APP_HOST: https://app.ppd.lecoffre.smart-chain.fr
NEXT_PUBLIC_FRONT_APP_PORT: "3000"
NEXT_PUBLIC_IDNOT_BASE_URL: "https://qual-connexion.idnot.fr"
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: "/IdPOAuth2/authorize/idnot_idp_v1"
NEXT_PUBLIC_IDNOT_CLIENT_ID: "4501646203F3EF67"
NEXT_PUBLIC_DOCAPOST_API_URL: "https://preprod.id360docaposte.com/api/1.0.0"
NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID: 49508376-b160-475d-9224-9bb6511215b8
NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID: 54c14875-f864-4819-8cd4-4fc6dd4a947b
NEXT_PUBLIC_DOCAPOST_APP_ID: leCoffre@smart_chain
context: context:
- sc-shared-prd - sc-shared-prd
filters: filters:
@ -88,6 +179,21 @@ workflows:
build-and-register-prd: build-and-register-prd:
jobs: jobs:
- build-push-docker-image: - build-push-docker-image:
NEXT_PUBLIC_BACK_API_HOST: api.lecoffre.smart-chain.fr
NEXT_PUBLIC_BACK_API_PROTOCOL: https://
NEXT_PUBLIC_BACK_API_ROOT_URL: /api
NEXT_PUBLIC_BACK_API_VERSION: /v1
NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT: https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize
NEXT_PUBLIC_FC_CLIENT_ID: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e
NEXT_PUBLIC_FRONT_APP_HOST: https://app.lecoffre.smart-chain.fr
NEXT_PUBLIC_FRONT_APP_PORT: "3000"
NEXT_PUBLIC_IDNOT_BASE_URL: "https://qual-connexion.idnot.fr"
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: "/IdPOAuth2/authorize/idnot_idp_v1"
NEXT_PUBLIC_IDNOT_CLIENT_ID: "4501646203F3EF67"
NEXT_PUBLIC_DOCAPOST_API_URL: "https://preprod.id360docaposte.com/api/1.0.0"
NEXT_PUBLIC_DOCAPOST_DOCUMENT_PROCESS_ID: 49508376-b160-475d-9224-9bb6511215b8
NEXT_PUBLIC_DOCAPOST_CONNECT_PROCESS_ID: 54c14875-f864-4819-8cd4-4fc6dd4a947b
NEXT_PUBLIC_DOCAPOST_APP_ID: leCoffre@smart_chain
context: context:
- sc-shared-prd - sc-shared-prd
filters: filters:

View File

@ -22,6 +22,7 @@ WORKDIR leCoffre-front
COPY --from=deps leCoffre-front/node_modules ./node_modules COPY --from=deps leCoffre-front/node_modules ./node_modules
COPY --from=deps leCoffre-front/package.json package.json COPY --from=deps leCoffre-front/package.json package.json
COPY tsconfig.json tsconfig.json COPY tsconfig.json tsconfig.json
COPY next.config.js next.config.js
COPY src src COPY src src
RUN npm run build RUN npm run build
@ -36,6 +37,7 @@ RUN adduser -D lecoffreuser --uid 10000 && chown -R lecoffreuser .
COPY public ./public COPY public ./public
COPY --from=builder --chown=lecoffreuser leCoffre-front/node_modules ./node_modules COPY --from=builder --chown=lecoffreuser leCoffre-front/node_modules ./node_modules
COPY --from=builder --chown=lecoffreuser leCoffre-front/.next ./.next COPY --from=builder --chown=lecoffreuser leCoffre-front/.next ./.next
COPY --from=builder --chown=lecoffreuser leCoffre-front/next.config.js ./next.config.js
COPY --from=builder --chown=lecoffreuser leCoffre-front/package.json ./package.json COPY --from=builder --chown=lecoffreuser leCoffre-front/package.json ./package.json
USER lecoffreuser USER lecoffreuser

View File

@ -15,6 +15,33 @@ const nextConfig = {
NEXT_PUBLIC_IDNOT_BASE_URL: process.env.NEXT_PUBLIC_IDNOT_BASE_URL, NEXT_PUBLIC_IDNOT_BASE_URL: process.env.NEXT_PUBLIC_IDNOT_BASE_URL,
NEXT_PUBLIC_DOCAPOSTE_API_URL: process.env.NEXT_PUBLIC_DOCAPOSTE_API_URL, NEXT_PUBLIC_DOCAPOSTE_API_URL: process.env.NEXT_PUBLIC_DOCAPOSTE_API_URL,
}, },
serverRuntimeConfig: {
NEXT_PUBLIC_BACK_API_PROTOCOL: process.env.NEXT_PUBLIC_BACK_API_PROTOCOL,
NEXT_PUBLIC_BACK_API_HOST: process.env.NEXT_PUBLIC_BACK_API_HOST,
NEXT_PUBLIC_BACK_API_ROOT_URL: process.env.NEXT_PUBLIC_BACK_API_ROOT_URL,
NEXT_PUBLIC_BACK_API_VERSION: process.env.NEXT_PUBLIC_BACK_API_VERSION,
NEXT_PUBLIC_FRONT_APP_HOST: process.env.NEXT_PUBLIC_FRONT_APP_HOST,
NEXT_PUBLIC_FRONT_APP_PORT: process.env.NEXT_PUBLIC_FRONT_APP_PORT,
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: process.env.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT,
NEXT_PUBLIC_IDNOT_CLIENT_ID: process.env.NEXT_PUBLIC_IDNOT_CLIENT_ID,
NEXT_PUBLIC_IDNOT_BASE_URL: process.env.NEXT_PUBLIC_IDNOT_BASE_URL,
NEXT_PUBLIC_DOCAPOSTE_API_URL: process.env.NEXT_PUBLIC_DOCAPOSTE_API_URL,
},
env: {
NEXT_PUBLIC_BACK_API_PROTOCOL: process.env.NEXT_PUBLIC_BACK_API_PROTOCOL,
NEXT_PUBLIC_BACK_API_HOST: process.env.NEXT_PUBLIC_BACK_API_HOST,
NEXT_PUBLIC_BACK_API_ROOT_URL: process.env.NEXT_PUBLIC_BACK_API_ROOT_URL,
NEXT_PUBLIC_BACK_API_VERSION: process.env.NEXT_PUBLIC_BACK_API_VERSION,
NEXT_PUBLIC_FRONT_APP_HOST: process.env.NEXT_PUBLIC_FRONT_APP_HOST,
NEXT_PUBLIC_FRONT_APP_PORT: process.env.NEXT_PUBLIC_FRONT_APP_PORT,
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT: process.env.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT,
NEXT_PUBLIC_IDNOT_CLIENT_ID: process.env.NEXT_PUBLIC_IDNOT_CLIENT_ID,
NEXT_PUBLIC_IDNOT_BASE_URL: process.env.NEXT_PUBLIC_IDNOT_BASE_URL,
NEXT_PUBLIC_DOCAPOSTE_API_URL: process.env.NEXT_PUBLIC_DOCAPOSTE_API_URL,
},
// webpack: config => { // webpack: config => {
// config.node = { // config.node = {
// fs: 'empty', // fs: 'empty',

550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.94", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.104",
"next": "13.2.4", "next": "13.2.4",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"react": "18.2.0", "react": "18.2.0",

View File

@ -0,0 +1,118 @@
import BaseApiService from "@Front/Api/BaseApiService";
import { ICustomerTokens } from "../Id360/Customers/Customers";
import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes";
export type IMailVerifyParams = {
email: string;
};
export type IMailVerifyReturn = {
partialPhoneNumber: string;
totpCodeUid: string;
};
export type IVerifyTotpCodeParams = {
totpCode: string;
email: string;
};
export type IVerifyTotpCodeReturn = {
validCode: boolean;
reason: TotpCodesReasons;
};
export type ISetPasswordParams = {
password: string;
email: string;
totpCode: string;
};
export type ILoginParams = {
password: string;
email: string;
totpCode: string;
};
export type IAskNewPasswordParams = {
email: string;
};
export type IAskAnotherCodeParams = {
email: string;
totpCodeUid: string;
};
export type IAskAnotherCodeReturn = {
partialPhoneNumber: string;
totpCodeUid: string;
};
export default class Auth extends BaseApiService {
private static instance: Auth;
protected readonly namespaceUrl = this.getBaseUrl().concat("/customer");
private readonly baseURl = this.namespaceUrl.concat("/auth");
public static getInstance() {
return (this.instance ??= new this());
}
public async mailVerifySms(body: IMailVerifyParams): Promise<IMailVerifyReturn> {
const url = new URL(this.baseURl.concat("/mail/verify-sms"));
try {
return this.postRequest<IMailVerifyReturn>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async verifyTotpCode(body: IVerifyTotpCodeParams): Promise<IVerifyTotpCodeReturn> {
const url = new URL(this.baseURl.concat("/verify-totp-code"));
try {
return this.postRequest<IVerifyTotpCodeReturn>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async setPassword(body: ISetPasswordParams): Promise<ICustomerTokens> {
const url = new URL(this.baseURl.concat("/set-password"));
try {
return this.postRequest<ICustomerTokens>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async login(body: ILoginParams): Promise<ICustomerTokens> {
const url = new URL(this.baseURl.concat("/login"));
try {
return this.postRequest<ICustomerTokens>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async askNewPassword(body: IAskNewPasswordParams): Promise<IMailVerifyReturn> {
const url = new URL(this.baseURl.concat("/ask-new-password"));
try {
return this.postRequest<IMailVerifyReturn>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async sendAnotherCode(body: IAskAnotherCodeParams): Promise<IAskAnotherCodeReturn> {
const url = new URL(this.baseURl.concat("/send-another-code"));
try {
return this.postRequest<IAskAnotherCodeReturn>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -138,11 +138,16 @@ export default abstract class BaseApiService {
} }
} else { } else {
// Handle error response // Handle error response
const responseCopy = response.clone();
try { try {
const responseJson = await response.json(); const responseJson = await response.json();
return Promise.reject(responseJson); return Promise.reject(responseJson);
} catch (err) { } catch (err) {
return Promise.reject(err); const responseText = await responseCopy.text();
return Promise.reject({
http_status: response.status,
message: responseText,
});
} }
} }

View File

@ -40,6 +40,7 @@ type IState = {
refusedReason?: string; refusedReason?: string;
isShowRefusedReasonModalVisible: boolean; isShowRefusedReasonModalVisible: boolean;
showFailedUploaded: string | null; showFailedUploaded: string | null;
showFailedDocument: string | null;
isAddDocumentModalVisible: boolean; isAddDocumentModalVisible: boolean;
isLoading: boolean; isLoading: boolean;
}; };
@ -59,6 +60,7 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
isShowRefusedReasonModalVisible: false, isShowRefusedReasonModalVisible: false,
showFailedUploaded: null, showFailedUploaded: null,
isLoading: false, isLoading: false,
showFailedDocument: null,
}; };
this.addDocument = this.addDocument.bind(this); this.addDocument = this.addDocument.bind(this);
@ -159,11 +161,20 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
</div> </div>
</div> </div>
</Confirm> </Confirm>
{this.state.showFailedUploaded && ( {this.state.showFailedDocument && (
<Alert <Alert
onClose={this.onCloseAlertUpload} onClose={this.onCloseAlertUpload}
header={"Fichier non autorisé"} header={"L'ajout de Document n'est plus autorisé"}
isOpen={!!this.state.showFailedUploaded}> isOpen={!!this.state.showFailedDocument}>
<div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}>
{this.state.showFailedDocument}
</Typography>
</div>
</Alert>
)}
{this.state.showFailedUploaded && (
<Alert onClose={this.onCloseAlertUpload} header={"Fichier non autorisé"} isOpen={!!this.state.showFailedUploaded}>
<div className={classes["modal-content"]}> <div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}> <Typography typo={ITypo.P_16} className={classes["text"]}>
{this.state.showFailedUploaded} {this.state.showFailedUploaded}
@ -188,15 +199,20 @@ export default class DepositOtherDocument extends React.Component<IProps, IState
const filesArray = this.state.currentFiles; const filesArray = this.state.currentFiles;
if (!filesArray) return; if (!filesArray) return;
let documentCreated: Document = {} as Document;
const documentCreated = await Documents.getInstance().post({ try {
folder: { documentCreated = await Documents.getInstance().post({
uid: this.props.folder_uid, folder: {
}, uid: this.props.folder_uid,
depositor: { },
uid: this.props.customer_uid, depositor: {
}, uid: this.props.customer_uid,
}); },
});
} catch (e) {
this.setState({ showFailedDocument: "Le dossier est vérifié aucune modification n'est acceptée", isLoading: false });
return;
}
for (let i = 0; i < filesArray.length; i++) { for (let i = 0; i < filesArray.length; i++) {
const formData = new FormData(); const formData = new FormData();

View File

@ -38,6 +38,7 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
} }
public override render(): JSX.Element { public override render(): JSX.Element {
console.log(this.props);
const toast = this.props.toast; const toast = this.props.toast;
const style = { const style = {
"--data-duration": `${toast.time}ms`, "--data-duration": `${toast.time}ms`,
@ -98,7 +99,8 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
this.close(); this.close();
} }
private close() { private async close() {
await Toasts.getInstance().markRead(this.props.toast);
window.clearTimeout(this.closeTimeout); window.clearTimeout(this.closeTimeout);
this.setState({ this.setState({
willClose: true, willClose: true,
@ -110,7 +112,7 @@ class ToastElementClass extends React.Component<IPropsClass, IState> {
private async handleClick(e: React.MouseEvent) { private async handleClick(e: React.MouseEvent) {
if (this.props.toast.redirectUrl) { if (this.props.toast.redirectUrl) {
this.onClose(e); await this.onClose(e);
await this.props.router.push(this.props.toast.redirectUrl); await this.props.router.push(this.props.toast.redirectUrl);
this.props.router.reload(); this.props.router.reload();
} }

View File

@ -76,6 +76,30 @@ export default class UserFolderHeader extends React.Component<IProps, IState> {
private formatPhoneNumber(phoneNumber: string): string { private formatPhoneNumber(phoneNumber: string): string {
if (!phoneNumber) return ""; if (!phoneNumber) return "";
phoneNumber = phoneNumber.replace(/ /g, ""); phoneNumber = phoneNumber.replace(/ /g, "");
phoneNumber = phoneNumber.replace("+33", "0");
if (phoneNumber.length !== 10) {
// split the last 9 digits
const lastNineDigits = phoneNumber.slice(-9);
// get the country code
const countryCode = phoneNumber.slice(0, -9);
// isolate the first digit
const firstDigit = lastNineDigits.slice(0, 1);
// isolate the 8 other ones
const lastEightDigits = lastNineDigits.slice(1);
// make a space every two digits on the last eights
const output = lastEightDigits.split("").map((char, index) => {
if (index % 2) return char + " ";
return char;
});
// format the phone number
phoneNumber = countryCode + " " + firstDigit + " " + output.join("");
return phoneNumber;
}
const output = phoneNumber.split("").map((char, index) => { const output = phoneNumber.split("").map((char, index) => {
if (index % 2) return char + " "; if (index % 2) return char + " ";
return char; return char;

View File

@ -163,7 +163,7 @@ export default function CollaboratorInformations(props: IProps) {
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Numéro de téléphone Numéro de téléphone
</Typography> </Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.phone_number}</Typography> <Typography typo={ITypo.P_18}>{userSelected?.contact?.cell_phone_number}</Typography>
</div> </div>
<div className={classes["user-infos-row"]}> <div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>

View File

@ -229,6 +229,16 @@ class AddClientToFolderClass extends BasePage<IPropsClass, IState> {
if (this.state.selectedOption === "new_customer") { if (this.state.selectedOption === "new_customer") {
try { try {
// remove every space from the phone number
values["cell_phone_number"] = values["cell_phone_number"].replace(/\s/g, "");
if (values["cell_phone_number"] && values["cell_phone_number"].length === 10) {
// get the first digit of the phone number
const firstDigit = values["cell_phone_number"].charAt(0);
// if the first digit is a 0 replace it by +33
if (firstDigit === "0") {
values["cell_phone_number"] = "+33" + values["cell_phone_number"].substring(1);
}
}
const contactToCreate = Contact.hydrate<Customer>(values); const contactToCreate = Contact.hydrate<Customer>(values);
await contactToCreate.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false }); await contactToCreate.validateOrReject?.({ groups: ["createCustomer"], forbidUnknownValues: false });
} catch (validationErrors) { } catch (validationErrors) {

View File

@ -107,7 +107,7 @@
justify-content: center; justify-content: center;
height: 100%; height: 100%;
.loader { .loader {
width: 40px; width: 21px;
height: 40px; height: 21px;
} }
} }

View File

@ -176,6 +176,17 @@ class UpdateClientClass extends BasePage<IPropsClass, IState> {
[key: string]: string; [key: string]: string;
}, },
) { ) {
if (!values["cell_phone_number"]) return;
// remove every space from the phone number
values["cell_phone_number"] = values["cell_phone_number"].replace(/\s/g, "");
if (values["cell_phone_number"] && values["cell_phone_number"].length === 10) {
// get the first digit of the phone number
const firstDigit = values["cell_phone_number"].charAt(0);
// if the first digit is a 0 replace it by +33
if (firstDigit === "0") {
values["cell_phone_number"] = "+33" + values["cell_phone_number"].substring(1);
}
}
const contact = Contact.hydrate<Contact>({ const contact = Contact.hydrate<Contact>({
first_name: values["first_name"], first_name: values["first_name"],
last_name: values["last_name"], last_name: values["last_name"],

View File

@ -0,0 +1,29 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
max-width: 530px;
margin: auto;
margin-top: 220px;
.title {
text-align: left;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.form {
margin-top: 32px;
.password_indication {
margin-top: 8px;
margin-bottom: 24px;
}
.submit_button {
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,43 @@
import React from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import Form from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { ValidationError } from "class-validator";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
};
export default function PasswordForgotten(props: IProps) {
const { onSubmit, validationErrors } = props;
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Réinitialisez votre mot de passe</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Mot de passe"
name="password"
validationError={validationErrors.find((error) => error.property === "password")}
password
/>
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.GREY} className={classes["password_indication"]}>
Au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.
</Typography>
<TextField
placeholder="Confirmation du mot de passe"
name="confirm_password"
validationError={validationErrors.find((error) => error.property === "confirm_password")}
password
/>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Valider
</Button>
</Form>
</div>
);
}

View File

@ -0,0 +1,49 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
max-width: 530px;
margin: auto;
margin-top: 220px;
.title {
margin-bottom: 32px;
text-align: left;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.logo {
margin-top: 32px;
cursor: pointer;
}
.what_is_france_connect {
color: var(--light-text-action-high-blue-france, #000091);
/* 2.Corps de texte/SM - Texte détail/Desktop & Mobile - Regular */
font-family: Marianne;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 171.429% */
margin-top: 12px;
}
.or {
margin-top: 32px;
}
.forget-password {
margin-top: 32px;
margin-bottom: 8px;
}
.form {
margin-top: 32px;
.submit_button {
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,62 @@
import React from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
//import Image from "next/image";
import Form from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
//import franceConnectLogo from "../france-connect.svg";
// import { useRouter } from "next/router";
// import Customers from "@Front/Api/Auth/Id360/Customers/Customers";
import { ValidationError } from "class-validator";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
};
export default function StepEmail(props: IProps) {
const { onSubmit, validationErrors } = props;
/* const router = useRouter();
const redirectCustomerOnConnection = useCallback(() => {
async function getCustomer() {
try {
const loginRes = await Customers.getInstance().login();
router.push(loginRes.enrollment.franceConnectUrl);
} catch (e) {
console.error(e);
}
}
getCustomer();
}, [router]); */
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Identifiez-vous</div>
</Typography>
{/* <Typography typo={ITypo.P_16}>Pour accéder à votre espace de dépôt des documents, veuillez vous identifier.</Typography>
<Image alt="france-connect" src={franceConnectLogo} onClick={redirectCustomerOnConnection} className={classes["logo"]} />
<div className={classes["what_is_france_connect"]}>Qu'est ce que FranceConnect ?</div>
<Typography className={classes["or"]} typo={ITypo.P_16}>
Ou
</Typography> */}
<Typography typo={ITypo.P_16}>Pour accéder à votre espace de dépôt des documents, veuillez vous identifier. </Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="E-mail"
name="email"
validationError={validationErrors.find((error) => error.property === "email")}
/>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Suivant
</Button>
</Form>
{/* <Typography typo={ITypo.P_18}>
<div className={classes["forget-password"]}>Vous n'arrivez pas à vous connecter ?</div>
</Typography>
<Link href="mailto:g.texier@notaires.fr">
<Button variant={EButtonVariant.LINE}>Contacter l'administrateur</Button>
</Link> */}
</div>
);
}

View File

@ -0,0 +1,29 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
max-width: 530px;
margin: auto;
margin-top: 220px;
.title {
text-align: left;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.form {
margin-top: 32px;
.password_indication {
margin-top: 8px;
margin-bottom: 24px;
}
.submit_button {
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,43 @@
import React from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import Form from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { ValidationError } from "class-validator";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
};
export default function StepNewPassword(props: IProps) {
const { onSubmit, validationErrors } = props;
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Configurez votre mot de passe</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Mot de passe"
name="password"
validationError={validationErrors.find((error) => error.property === "password")}
password
/>
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.GREY} className={classes["password_indication"]}>
Au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.
</Typography>
<TextField
placeholder="Confirmation du mot de passe"
name="confirm_password"
validationError={validationErrors.find((error) => error.property === "confirm_password")}
password
/>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Valider
</Button>
</Form>
</div>
);
}

View File

@ -0,0 +1,30 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
max-width: 530px;
margin: auto;
margin-top: 220px;
.title {
text-align: left;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.form {
margin-top: 32px;
.submit_button {
margin-top: 32px;
}
.forgot-password {
margin-top: 8px;
text-decoration: underline;
cursor: pointer;
}
}
}

View File

@ -0,0 +1,70 @@
import React from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
import Form from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { ValidationError } from "class-validator";
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
onPasswordForgotClicked: () => void;
};
export default function StepPassword(props: IProps) {
const { onSubmit, validationErrors, onPasswordForgotClicked } = props;
const [isModalOpened, setIsModalOpened] = React.useState(false);
const closeModal = () => {
setIsModalOpened(false);
};
const openModal = () => {
setIsModalOpened(true);
};
const onModalAccept = () => {
onPasswordForgotClicked();
setIsModalOpened(false);
};
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Entrez votre mot de passe</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Mot de passe"
name="password"
validationError={validationErrors.find((error) => error.property === "password")}
password
/>
<div onClick={openModal}>
<Typography typo={ITypo.P_16} className={classes["forgot-password"]}>
Mot de passe oublié ?
</Typography>
</div>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Valider
</Button>
</Form>
<Confirm
isOpen={isModalOpened}
onClose={closeModal}
showCancelButton={true}
onAccept={onModalAccept}
closeBtn
header={"Mot de passe oublié ?"}
confirmText={"Valider"}
cancelText={"Annuler"}>
<div className={classes["modal-content"]}>
<Typography typo={ITypo.P_16} className={classes["text"]}>
Un code à usage unique va vous être envoyé par sms pour réinitialiser votre mot de passe.
</Typography>
</div>
</Confirm>
</div>
);
}

View File

@ -0,0 +1,43 @@
@import "@Themes/constants.scss";
.root {
display: flex;
flex-direction: column;
max-width: 530px;
margin: 220px auto;
.title {
text-align: left;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.form {
margin-top: 32px;
.submit_button {
margin-top: 32px;
}
}
.ask-another-code {
margin-top: 48px;
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
.new-code-button {
&[data-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}
}
.new-code-timer {
display: flex;
gap: 6px;
align-items: center;
}
}
}

View File

@ -0,0 +1,76 @@
import React, { useEffect } from "react";
import classes from "./classes.module.scss";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import Form from "@Front/Components/DesignSystem/Form";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import { ValidationError } from "class-validator";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
partialPhoneNumber: string;
onSendAnotherCode: () => void;
};
export default function StepTotp(props: IProps) {
const { onSubmit, validationErrors, partialPhoneNumber, onSendAnotherCode } = props;
const [disableNewCodeButton, setDisableNewCodeButton] = React.useState(false);
const [secondsBeforeNewCode, setSecondsBeforeNewCode] = React.useState(0);
useEffect(() => {
const interval = setInterval(() => {
if (secondsBeforeNewCode > 0) {
setSecondsBeforeNewCode(secondsBeforeNewCode - 1);
if (secondsBeforeNewCode === 1) {
setDisableNewCodeButton(false);
}
}
}, 1000);
return () => clearInterval(interval);
}, [secondsBeforeNewCode]);
const sendAnotherCode = () => {
onSendAnotherCode();
setDisableNewCodeButton(true);
setSecondsBeforeNewCode(30);
};
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>
Votre code a é envoyé par SMS au ** ** ** {partialPhoneNumber.replace(/(.{2})/g, "$1 ")}
</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Code à usage unique"
name="totpCode"
validationError={validationErrors.find((error) => error.property === "totpCode")}
/>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Suivant
</Button>
</Form>
<div className={classes["ask-another-code"]}>
<Typography typo={ITypo.P_16}>Vous n'avez rien reçu ?</Typography>
<Button
variant={EButtonVariant.LINE}
disabled={disableNewCodeButton}
data-disabled={disableNewCodeButton.toString()}
onClick={sendAnotherCode}
className={classes["new-code-button"]}>
Envoyer un nouveau code
</Button>
{secondsBeforeNewCode !== 0 && (
<Typography typo={ITypo.P_SB_16} className={classes["new-code-timer"]}>
Redemandez un code dans
<Typography typo={ITypo.P_16} color={ITypoColor.PINK_FLASH}>
00:{secondsBeforeNewCode < 10 ? `0${secondsBeforeNewCode}` : secondsBeforeNewCode}
</Typography>
</Typography>
)}
</div>
</div>
);
}

View File

@ -1,29 +1,4 @@
@import "@Themes/constants.scss"; @import "@Themes/constants.scss";
.root { .root {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
max-width: 530px;
margin: auto;
.title {
margin: 32px 0;
text-align: center;
@media (max-width: $screen-s) {
font-family: 48px;
}
}
.forget-password {
margin-top: 32px;
margin-bottom: 8px;
}
.logo {
cursor: pointer;
}
} }

View File

@ -1,34 +1,41 @@
import CoffreIcon from "@Assets/Icons/coffre.svg";
import franceConnectLogo from "./france-connect.svg";
import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button";
import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage"; import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import Customers from "@Front/Api/Auth/Id360/Customers/Customers";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import LandingImage from "./landing-connect.jpeg"; import LandingImage from "./landing-connect.jpeg";
import Link from "next/link";
import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; import Confirm from "@Front/Components/DesignSystem/Modal/Confirm";
import StepEmail from "./StepEmail";
import StepTotp from "./StepTotp";
import Auth from "@Front/Api/Auth/Customer/Auth";
import { ValidationError } from "class-validator";
import StepPassword from "./StepPassword";
import StepNewPassword from "./StepNewPassword";
import CustomerStore from "@Front/Stores/CustomerStore";
import Module from "@Front/Config/Module";
import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes";
import PasswordForgotten from "./PasswordForgotten";
export enum LoginStep {
EMAIL,
TOTP,
PASSWORD,
NEW_PASSWORD,
PASSWORD_FORGOTTEN,
}
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const error = router.query["error"]; const error = router.query["error"];
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
const redirectCustomerOnConnection = useCallback(() => { const [step, setStep] = useState<LoginStep>(LoginStep.EMAIL);
async function getCustomer() { const [totpCodeUid, setTotpCodeUid] = useState<string>("");
try { const [totpCode, setTotpCode] = useState<string>("");
const loginRes = await Customers.getInstance().login(); const [email, setEmail] = useState<string>("");
router.push(loginRes.enrollment.franceConnectUrl); const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>("");
} catch (e) { const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
console.error(e);
}
}
getCustomer();
}, [router]);
const openErrorModal = useCallback(() => { const openErrorModal = useCallback(() => {
setIsErrorModalOpen(true); setIsErrorModalOpen(true);
@ -42,20 +49,183 @@ export default function Login() {
if (error === "1") openErrorModal(); if (error === "1") openErrorModal();
}, [error, openErrorModal]); }, [error, openErrorModal]);
const onEmailFormSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
try {
if (!values["email"]) return;
setEmail(values["email"]);
const res = await Auth.getInstance().mailVerifySms({ email: values["email"] });
setPartialPhoneNumber(res.partialPhoneNumber);
setTotpCodeUid(res.totpCodeUid);
setStep(LoginStep.TOTP);
} catch (error: any) {
setValidationErrors([
{
property: "email",
constraints: {
[error.http_status]: error.message,
},
},
]);
return;
}
}, []);
const onSmsCodeSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
try {
if (!values["totpCode"]) return;
const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email });
// If the code is valid setting it in state
if (res.validCode) setTotpCode(values["totpCode"]);
// If it's first connection, show the form for first connection
if (res.reason === TotpCodesReasons.FIRST_LOGIN) setStep(LoginStep.NEW_PASSWORD);
// If it's password forgotten, show the form for password forgotten
else if (res.reason === TotpCodesReasons.RESET_PASSWORD) setStep(LoginStep.PASSWORD_FORGOTTEN);
// Else just login normally
else setStep(LoginStep.PASSWORD);
} catch (error: any) {
setValidationErrors([
{
property: "totpCode",
constraints: {
[error.http_status]: error.message,
},
},
]);
return;
}
},
[email, setStep],
);
const onNewPasswordSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
try {
if (!values["password"] || !values["confirm_password"]) return;
if (values["password"] !== values["confirm_password"]) {
setValidationErrors([
{
property: "confirm_password",
constraints: {
"400": "Les mots de passe ne correspondent pas.",
},
},
]);
return;
}
const passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/);
if (!passwordRegex.test(values["password"])) {
setValidationErrors([
{
property: "password",
constraints: {
"400": "Le mot de passe doit contenir au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.",
},
},
]);
return;
}
const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] });
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
// If set password worked, setting the token and redirecting
} catch (error: any) {
setValidationErrors([
{
property: "password",
constraints: {
[error.http_status]: error.message,
},
},
]);
return;
}
},
[totpCode, email, router],
);
const onPasswordSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => {
try {
if (!values["password"]) return;
const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] });
CustomerStore.instance.connect(token.accessToken, token.refreshToken);
router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path);
} catch (error: any) {
setValidationErrors([
{
property: "password",
constraints: {
[error.http_status]: error.message,
},
},
]);
return;
}
},
[email, router, totpCode],
);
const onPasswordForgotClicked = useCallback(async () => {
try {
const res = await Auth.getInstance().askNewPassword({ email });
setPartialPhoneNumber(res.partialPhoneNumber);
setStep(LoginStep.TOTP);
} catch (error: any) {
// If token already exists and is still valid redirect to the connect/register page
if (error.http_status === 425) {
setStep(LoginStep.TOTP);
return;
}
return;
}
}, [email]);
const onSendAnotherCode = useCallback(async () => {
try {
const res = await Auth.getInstance().sendAnotherCode({ email, totpCodeUid });
setPartialPhoneNumber(res.partialPhoneNumber);
setTotpCodeUid(res.totpCodeUid);
} catch (error: any) {
setValidationErrors([
{
property: "totpCode",
constraints: {
[error.http_status]: error.message,
},
},
]);
return;
}
}, [email, totpCodeUid]);
return ( return (
<DefaultDoubleSidePage title={"Login"} image={LandingImage}> <DefaultDoubleSidePage title={"Login"} image={LandingImage}>
<div className={classes["root"]}> <div className={classes["root"]}>
<Image alt="coffre" src={CoffreIcon} /> {step === LoginStep.EMAIL && <StepEmail onSubmit={onEmailFormSubmit} validationErrors={validationErrors} />}
<Typography typo={ITypo.H1}> {step === LoginStep.TOTP && (
<div className={classes["title"]}>Connexion espace client</div> <StepTotp
</Typography> onSubmit={onSmsCodeSubmit}
<Image alt="france-connect" src={franceConnectLogo} onClick={redirectCustomerOnConnection} className={classes["logo"]} /> validationErrors={validationErrors}
<Typography typo={ITypo.P_18}> partialPhoneNumber={partialPhoneNumber}
<div className={classes["forget-password"]}>Vous n'arrivez pas à vous connecter ?</div> onSendAnotherCode={onSendAnotherCode}
</Typography> />
<Link href="mailto:g.texier@notaires.fr"> )}
<Button variant={EButtonVariant.LINE}>Contacter l'administrateur</Button> {step === LoginStep.PASSWORD && (
</Link> <StepPassword
onSubmit={onPasswordSubmit}
validationErrors={validationErrors}
onPasswordForgotClicked={onPasswordForgotClicked}
/>
)}
{step === LoginStep.NEW_PASSWORD && <StepNewPassword onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />}
{step === LoginStep.PASSWORD_FORGOTTEN && (
<PasswordForgotten onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />
)}
</div> </div>
<Confirm <Confirm
isOpen={isErrorModalOpen} isOpen={isErrorModalOpen}

View File

@ -61,7 +61,7 @@ export default class MyAccount extends Base<IProps, IState> {
<TextField <TextField
name="phone" name="phone"
placeholder="Numéro de téléphone" placeholder="Numéro de téléphone"
defaultValue={this.state.user?.contact?.phone_number as string} defaultValue={this.state.user?.contact?.cell_phone_number as string}
disabled disabled
canCopy canCopy
/> />

View File

@ -173,7 +173,7 @@ export default function UserInformations(props: IProps) {
const liveVote = await LiveVotes.getInstance().post(vote); const liveVote = await LiveVotes.getInstance().post(vote);
if (liveVote.appointment.votes?.length === 3) { if (liveVote.appointment.votes?.length === 3) {
if(superAdminModalType === "add") { if (superAdminModalType === "add") {
Toasts.getInstance().open({ Toasts.getInstance().open({
title: `Le titre de super-administrateur a été attribué à ${userSelected.contact?.first_name} ${userSelected.contact?.last_name} `, title: `Le titre de super-administrateur a été attribué à ${userSelected.contact?.first_name} ${userSelected.contact?.last_name} `,
}); });
@ -238,7 +238,7 @@ export default function UserInformations(props: IProps) {
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
Numéro de téléphone Numéro de téléphone
</Typography> </Typography>
<Typography typo={ITypo.P_18}>{userSelected?.contact?.phone_number}</Typography> <Typography typo={ITypo.P_18}>{userSelected?.contact?.cell_phone_number}</Typography>
</div> </div>
<div className={classes["user-infos-row"]}> <div className={classes["user-infos-row"]}>
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>

View File

@ -85,15 +85,18 @@ export default class Toasts {
return () => this.close(toast); return () => this.close(toast);
} }
public markRead(toast: IToast) {
if (!toast.uid) return;
Notifications.getInstance().put(toast.uid, {
read: true,
});
}
public close(toast: IToast) { public close(toast: IToast) {
const index = this.toastList.indexOf(toast); const index = this.toastList.indexOf(toast);
if (index === -1) return; if (index === -1) return;
this.toastList.splice(index, 1); this.toastList.splice(index, 1);
if (toast.uid)
Notifications.getInstance().put(toast.uid, {
read: true,
});
this.event.emit("change", this.toastList); this.event.emit("change", this.toastList);
} }

View File

@ -4,6 +4,7 @@ import "@Front/index.scss";
import type { NextPage } from "next"; import type { NextPage } from "next";
import type { AppType, AppProps } from "next/app"; import type { AppType, AppProps } from "next/app";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
import getConfig from 'next/config'
export type NextPageWithLayout<TProps = Record<string, unknown>, TInitialProps = TProps> = NextPage<TProps, TInitialProps> & { export type NextPageWithLayout<TProps = Record<string, unknown>, TInitialProps = TProps> = NextPage<TProps, TInitialProps> & {
getLayout?: (page: ReactElement) => ReactNode; getLayout?: (page: ReactElement) => ReactNode;
@ -25,6 +26,8 @@ type AppPropsWithLayout = AppProps & {
docaposteApiUrl: string; docaposteApiUrl: string;
}; };
const { publicRuntimeConfig } = getConfig();
const MyApp = (({ const MyApp = (({
Component, Component,
pageProps, pageProps,
@ -43,16 +46,16 @@ const MyApp = (({
const getLayout = Component.getLayout ?? ((page) => <DefaultLayout children={page}></DefaultLayout>); const getLayout = Component.getLayout ?? ((page) => <DefaultLayout children={page}></DefaultLayout>);
const instance = FrontendVariables.getInstance(); const instance = FrontendVariables.getInstance();
instance.BACK_API_PROTOCOL = backApiProtocol ?? "https://"; instance.BACK_API_PROTOCOL = backApiProtocol;
instance.BACK_API_HOST = backApiHost ?? "api.stg.lecoffre.smart-chain.fr"; instance.BACK_API_HOST = backApiHost;
instance.BACK_API_ROOT_URL = backApiRootUrl ?? "/api"; instance.BACK_API_ROOT_URL = backApiRootUrl;
instance.BACK_API_VERSION = backApiVersion ?? "/v1"; instance.BACK_API_VERSION = backApiVersion;
instance.FRONT_APP_HOST = frontAppHost ?? "https://app.stg.lecoffre.smart-chain.fr"; instance.FRONT_APP_HOST = frontAppHost;
instance.IDNOT_BASE_URL = idNotBaseUrl ?? "https://qual-connexion.idnot.fr"; instance.IDNOT_BASE_URL = idNotBaseUrl;
instance.IDNOT_AUTHORIZE_ENDPOINT = idNotAuthorizeEndpoint ?? "/IdPOAuth2/authorize/idnot_idp_v1"; instance.IDNOT_AUTHORIZE_ENDPOINT = idNotAuthorizeEndpoint;
instance.IDNOT_CLIENT_ID = idNotClientId ?? "4501646203F3EF67"; instance.IDNOT_CLIENT_ID = idNotClientId;
instance.FC_AUTHORIZE_ENDPOINT= fcAuthorizeEndpoint ?? "https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize"; instance.FC_AUTHORIZE_ENDPOINT= fcAuthorizeEndpoint;
instance.FC_CLIENT_ID = fcClientId ?? "211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e"; instance.FC_CLIENT_ID = fcClientId;
instance.DOCAPOST_API_URL = docaposteApiUrl; instance.DOCAPOST_API_URL = docaposteApiUrl;
return getLayout(<Component {...pageProps} />); return getLayout(<Component {...pageProps} />);
@ -60,18 +63,18 @@ const MyApp = (({
MyApp.getInitialProps = async () => { MyApp.getInitialProps = async () => {
return { return {
backApiProtocol: process.env["NEXT_PUBLIC_BACK_API_PROTOCOL"], backApiProtocol: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_PROTOCOL,
backApiHost: process.env["NEXT_PUBLIC_BACK_API_HOST"], backApiHost: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_HOST,
backApiRootUrl: process.env["NEXT_PUBLIC_BACK_API_ROOT_URL"], backApiRootUrl: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_ROOT_URL,
backApiVersion: process.env["NEXT_PUBLIC_BACK_API_VERSION"], backApiVersion: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_VERSION,
frontAppHost: process.env["NEXT_PUBLIC_FRONT_APP_HOST"], frontAppHost: publicRuntimeConfig.NEXT_PUBLIC_FRONT_APP_HOST,
frontAppPort: process.env["NEXT_PUBLIC_FRONT_APP_PORT"], frontAppPort: publicRuntimeConfig.NEXT_PUBLIC_FRONT_APP_PORT,
idNotBaseUrl: process.env["NEXT_PUBLIC_IDNOT_BASE_URL"], idNotBaseUrl: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_BASE_URL,
idNotAuthorizeEndpoint: process.env["NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT"], idNotAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT,
idNotClientId: process.env["NEXT_PUBLIC_IDNOT_CLIENT_ID"], idNotClientId: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_CLIENT_ID,
fcAuthorizeEndpoint: process.env["NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT"], fcAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT,
fcClientId: process.env["NEXT_PUBLIC_FC_CLIENT_ID"], fcClientId: publicRuntimeConfig.NEXT_PUBLIC_FC_CLIENT_ID,
docaposteApiUrl: process.env["NEXT_PUBLIC_DOCAPOST_API_URL"], docaposteApiUrl: publicRuntimeConfig.NEXT_PUBLIC_DOCAPOST_API_URL,
}; };
}; };