From dfecbc306aaac9dd8faca7e16be062aaa250c795 Mon Sep 17 00:00:00 2001 From: Bastien DUMONT <bdumont@grandlyon.com> Date: Fri, 12 Apr 2024 08:15:29 +0000 Subject: [PATCH] feat(grdf)!: update error messages --- .vscode/settings.json | 1 + app.config.environment.alpha.js | 11 +- app.config.environment.dev.js | 3 + app.config.environment.prod.js | 5 +- .../expiredConsentModal.scss | 2 +- .../GRDFConnect/GrdfConnectView.tsx | 5 +- .../GRDFConnect/GrdfWaitConsent.scss | 9 +- .../GRDFConnect/GrdfWaitConsent.tsx | 49 ++++-- .../Consumption/ConsumptionView.tsx | 126 +++++++------- .../Consumption/FluidButtons/FluidButton.tsx | 2 +- .../ConnectionResult/ConnectionResult.tsx | 73 ++++---- .../Konnector/KonnectorModal.spec.tsx | 20 +-- src/components/Konnector/KonnectorModal.tsx | 70 ++++---- .../Konnector/KonnectorModalFooter.tsx | 152 +++++++++-------- .../Konnector/KonnectorViewerCard.tsx | 158 +++++++----------- .../Konnector/KonnectorViewerList.tsx | 3 +- .../KonnectorModal.spec.tsx.snap | 2 +- src/components/Konnector/konnectorModal.scss | 34 ++-- .../Options/ReportOptions/ReportOptions.tsx | 2 +- src/enums/fluid.enum.ts | 12 +- src/enums/konnectorStatus.enum.ts | 2 +- src/locales/fr.json | 23 +-- src/services/fluid.service.ts | 100 ++++++----- src/store/global/global.slice.spec.ts | 2 +- src/store/global/global.slice.ts | 5 +- src/utils/utils.spec.ts | 6 +- src/utils/utils.ts | 3 +- tests/__mocks__/fluidStatusData.mock.ts | 2 +- 28 files changed, 442 insertions(+), 440 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0e5f3bdeb..0107733e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -101,6 +101,7 @@ "grdfgrandlyon", "Hypervitesse", "késako", + "kmodal", "Konnected", "konnector", "konnectors", diff --git a/app.config.environment.alpha.js b/app.config.environment.alpha.js index 346a1c982..e3fe4c88a 100644 --- a/app.config.environment.alpha.js +++ b/app.config.environment.alpha.js @@ -19,22 +19,25 @@ module.exports = { __STACK_ASSETS__: target !== 'mobile', __PIWIK_TRACKER_URL__: JSON.stringify('https://statweb.grandlyon.com/'), __PIWIK_SITEID__: 117, - __SENTRY_DSN__: JSON.stringify( - 'https://c868f6010f3f431d95be8f70d7f37666@grandlyon.errors.cozycloud.cc/6' - ), __SAU_LINK__: JSON.stringify( 'https://portail-citoyen-sau.guichet-recette.grandlyon.com/ecolyo/' ), __SAU_IDEA_DIRECT_LINK__: JSON.stringify( 'https://demarches-sau.guichet-recette.grandlyon.com/retour-ecolyo/ecolyo-une-idee/' ), + __SAU_ISSUE_DIRECT_LINK__: JSON.stringify( + 'https://demarches-sau.guichet-recette.grandlyon.com/retour-ecolyo/ecolyo-un-probleme/' + ), + __SENTRY_DSN__: JSON.stringify( + 'https://c868f6010f3f431d95be8f70d7f37666@grandlyon.errors.cozycloud.cc/6' + ), }), ], optimization: { minimizer: [ new TerserPlugin({ parallel: true, - //To fix a SAfari 10 bug : https://github.com/zeit/next.js/issues/5630 + // To fix a SAfari 10 bug : https://github.com/zeit/next.js/issues/5630 terserOptions: { safari10: true, }, diff --git a/app.config.environment.dev.js b/app.config.environment.dev.js index 120aa2a2d..e8326334e 100644 --- a/app.config.environment.dev.js +++ b/app.config.environment.dev.js @@ -29,6 +29,9 @@ const stackProvidedLibsConfig = { __SAU_IDEA_DIRECT_LINK__: JSON.stringify( 'https://demarches-sau.guichet-recette.grandlyon.com/retour-ecolyo/ecolyo-une-idee/' ), + __SAU_ISSUE_DIRECT_LINK__: JSON.stringify( + 'https://demarches-sau.guichet-recette.grandlyon.com/retour-ecolyo/ecolyo-un-probleme/' + ), __SENTRY_DSN__: JSON.stringify( 'https://c868f6010f3f431d95be8f70d7f37666@grandlyon.errors.cozycloud.cc/6' ), diff --git a/app.config.environment.prod.js b/app.config.environment.prod.js index 6688983f2..b6b24c48c 100644 --- a/app.config.environment.prod.js +++ b/app.config.environment.prod.js @@ -23,6 +23,9 @@ module.exports = { __SAU_IDEA_DIRECT_LINK__: JSON.stringify( 'https://demarches-support.grandlyon.com/retour-ecolyo/ecolyo-une-idee/' ), + __SAU_ISSUE_DIRECT_LINK__: JSON.stringify( + 'https://demarches-support.grandlyon.com/retour-ecolyo/ecolyo-un-probleme/' + ), __SENTRY_DSN__: JSON.stringify( 'https://c868f6010f3f431d95be8f70d7f37666@grandlyon.errors.cozycloud.cc/6' ), @@ -32,7 +35,7 @@ module.exports = { minimizer: [ new TerserPlugin({ parallel: true, - //To fix a SAfari 10 bug : https://github.com/zeit/next.js/issues/5630 + // To fix a SAfari 10 bug : https://github.com/zeit/next.js/issues/5630 terserOptions: { safari10: true, }, diff --git a/src/components/Connection/ExpiredConsentModal/expiredConsentModal.scss b/src/components/Connection/ExpiredConsentModal/expiredConsentModal.scss index 929dea446..758ca8581 100644 --- a/src/components/Connection/ExpiredConsentModal/expiredConsentModal.scss +++ b/src/components/Connection/ExpiredConsentModal/expiredConsentModal.scss @@ -3,7 +3,7 @@ .expired-consent-modal { display: flex; flex-direction: column; - gap: 1rem; + gap: 24px; color: $grey-bright; .icon-main { diff --git a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx index 114afba3f..0ec4d939b 100644 --- a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx +++ b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx @@ -20,15 +20,14 @@ export enum GrdfStep { } export const GrdfConnectView = () => { - const [launchConnection, setLaunchConnection] = useState(false) const navigate = useNavigate() + const { data: instanceSettings } = useUserInstanceSettings() const { fluidStatus } = useAppSelector(state => state.ecolyo.global) const currentFluidStatus = fluidStatus[FluidType.GAS] const account = currentFluidStatus.connection.account - const { data: instanceSettings } = useUserInstanceSettings() + const [launchConnection, setLaunchConnection] = useState(false) const [currentStep, setCurrentStep] = useState<GrdfStep>(GrdfStep.Identity) - const [formData, setFormData] = useState<AccountGRDFData>({ lastname: '', firstname: '', diff --git a/src/components/Connection/GRDFConnect/GrdfWaitConsent.scss b/src/components/Connection/GRDFConnect/GrdfWaitConsent.scss index 674d6fb8b..a1e3cbbf8 100644 --- a/src/components/Connection/GRDFConnect/GrdfWaitConsent.scss +++ b/src/components/Connection/GRDFConnect/GrdfWaitConsent.scss @@ -2,17 +2,20 @@ .grdfWait { margin: auto; + margin-top: 16px; display: flex; flex-direction: column; - gap: 1rem; + gap: 16px; align-items: center; text-align: center; + padding-inline: 1rem; + max-width: 600px; .green { - color: var(--gasColor); + color: $gas-color; } - .emailContainer span { + .emailContainer { color: $gold-shadow; font-weight: 700; } diff --git a/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx b/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx index 7a9be5885..361ef2f05 100644 --- a/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx +++ b/src/components/Connection/GRDFConnect/GrdfWaitConsent.tsx @@ -1,22 +1,51 @@ import { Button } from '@material-ui/core' import GRDFMail from 'assets/icons/visu/onboarding/grdf-mail.svg' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' +import useUserInstanceSettings from 'components/Hooks/useUserInstanceSettings' import { useI18n } from 'cozy-ui/transpiled/react/I18n' -import React from 'react' +import { FluidType } from 'enums' +import { FluidConnection } from 'models' +import React, { useEffect, useState } from 'react' +import { updateFluidConnection } from 'store/global/global.slice' +import { useAppDispatch, useAppSelector } from 'store/hooks' import './GrdfWaitConsent.scss' export const GrdfWaitConsent = () => { const { t } = useI18n() + const dispatch = useAppDispatch() + const { fluidStatus } = useAppSelector(state => state.ecolyo.global) + const { data: instanceSettings } = useUserInstanceSettings() + const [emailSentOn, setEmailSentOn] = useState('') + const currentFluidStatus = fluidStatus[FluidType.GAS] + + useEffect(() => { + setEmailSentOn(instanceSettings.email || '') + }, [instanceSettings]) + + const updateKonnector = async () => { + const updatedConnection: FluidConnection = { + ...currentFluidStatus.connection, + // TODO : investigate is this is duplicate ? + shouldLaunchKonnector: true, + isUpdating: true, + } + dispatch( + updateFluidConnection({ + fluidType: currentFluidStatus.fluidType, + fluidConnection: updatedConnection, + }) + ) + } + return ( <div className="grdfWait"> - <div - className="text-18-normal emailContainer" - dangerouslySetInnerHTML={{ - __html: t('auth.grdfgrandlyon.waiting.mailSent', { - email: 'test@test.com', - }), - }} - /> + <div className="text-18-normal"> + {t('auth.grdfgrandlyon.waiting.mailSent')} + </div> + <div className="text-16-normal"> + {t('auth.grdfgrandlyon.waiting.mailDelay')} + </div> + <span className="emailContainer">{emailSentOn}</span> <StyledIcon size={80} icon={GRDFMail} /> <div className="text-18-normal"> <span className="text-18-bold green"> @@ -25,7 +54,7 @@ export const GrdfWaitConsent = () => { <br /> <span>{t('auth.grdfgrandlyon.waiting.comeback')}</span> </div> - <Button className="btnPrimary"> + <Button className="btnPrimary" onClick={updateKonnector}> {t('auth.grdfgrandlyon.waiting.button_done')} </Button> </div> diff --git a/src/components/Consumption/ConsumptionView.tsx b/src/components/Consumption/ConsumptionView.tsx index ae0db62cb..3bfe524ba 100644 --- a/src/components/Consumption/ConsumptionView.tsx +++ b/src/components/Consumption/ConsumptionView.tsx @@ -1,4 +1,5 @@ import ExpiredConsentModal from 'components/Connection/ExpiredConsentModal/ExpiredConsentModal' +import { GrdfWaitConsent } from 'components/Connection/GRDFConnect/GrdfWaitConsent' import Content from 'components/Content/Content' import CustomPopupModal from 'components/CustomPopup/CustomPopupModal' import DateNavigator from 'components/DateNavigator/DateNavigator' @@ -10,7 +11,7 @@ import KonnectorViewerList from 'components/Konnector/KonnectorViewerList' import PartnerIssueModal from 'components/PartnerIssue/PartnerIssueModal' import ReleaseNotesModal from 'components/ReleaseNotesModal/ReleaseNotesModal' import { useClient } from 'cozy-client' -import { FluidType, TimeStep } from 'enums' +import { FluidState, FluidType, TimeStep } from 'enums' import { DateTime } from 'luxon' import React, { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' @@ -28,7 +29,6 @@ import { openPartnersModal, setCustomPopup } from 'store/modal/modal.slice' import { isLastDateReached } from 'utils/date' import { getKonnectorUpdateError, - getPartnerKey, getTodayDate, isKonnectorActive, } from 'utils/utils' @@ -46,29 +46,46 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { } = useAppSelector(state => state.ecolyo) const isMulti = fluidType === FluidType.MULTIFLUID + const currentFluidStatus = fluidStatus[fluidType] const dateChartService = new DateChartService() + /** Show wait consent screen when consent is "A valider" */ + const isWaitingForConsent = + fluidType === FluidType.GAS && + currentFluidStatus.status === FluidState.CHALLENGE_ASKED + + const [openExpiredConsentModal, setOpenExpiredConsentModal] = useState(true) const [openReleaseNoteModal, setOpenReleaseNoteModal] = useState<boolean>( releaseNotes.show ) - - const [openExpiredConsentModal, setOpenExpiredConsentModal] = - useState<boolean>(true) const [consentExpiredFluids, setConsentExpiredFluids] = useState<FluidType[]>( [] ) const updateKey = - !isMulti && fluidStatus[fluidType].lastDataDate - ? `${fluidStatus[fluidType].lastDataDate!.toLocaleString()} + ${ - fluidStatus[fluidType].status + fluidType + !isMulti && currentFluidStatus.lastDataDate + ? `${currentFluidStatus.lastDataDate.toLocaleString()} + ${ + currentFluidStatus.status + fluidType }` : '' const lastDataDateKey = - !isMulti && fluidStatus[fluidType].lastDataDate - ? `${fluidStatus[fluidType].lastDataDate!.toLocaleString() + fluidType}` + !isMulti && currentFluidStatus.lastDataDate + ? `${currentFluidStatus.lastDataDate.toLocaleString() + fluidType}` : '' + const getPartnerKey = (fluidType: FluidType): 'enedis' | 'egl' | 'grdf' => { + switch (fluidType) { + case FluidType.ELECTRICITY: + return 'enedis' + case FluidType.WATER: + return 'egl' + case FluidType.GAS: + return 'grdf' + default: + throw new Error('unknown fluidtype') + } + } + const handleCloseReleaseNoteModal = useCallback(() => { setOpenReleaseNoteModal(false) dispatch( @@ -119,15 +136,18 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { } } - /** Handle time change */ - useEffect(() => { - if ( - fluidType !== FluidType.ELECTRICITY && - currentTimeStep == TimeStep.HALF_AN_HOUR - ) { - dispatch(setCurrentTimeStep(TimeStep.WEEK)) - } - }, [dispatch, fluidType, currentTimeStep]) + useEffect( + /** Reset half-hour timestep for water & gas & multifluid */ + function setDefaultTimeStep() { + if ( + fluidType !== FluidType.ELECTRICITY && + currentTimeStep == TimeStep.HALF_AN_HOUR + ) { + dispatch(setCurrentTimeStep(TimeStep.WEEK)) + } + }, + [dispatch, fluidType, currentTimeStep] + ) /** * If fluid is not connected, display Connect components @@ -138,10 +158,10 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { dispatch(setShowOfflineData(isFluidConnected)) }, [dispatch, fluidStatus, fluidType]) + /** Check if some fluids have expired consent error */ useEffect(() => { let subscribed = true const expiredConsents: FluidType[] = [] - // Check if some fluids have expired consent error for (const fluid of fluidStatus) { const error = fluid.connection.triggerState?.last_error if (error && getKonnectorUpdateError(error) === 'error_update_oauth') { @@ -205,38 +225,30 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { <Content> <FluidButtons activeFluid={fluidType} key={updateKey} /> - {openReleaseNoteModal && ( - <ReleaseNotesModal - open={openReleaseNoteModal} - handleCloseClick={handleCloseReleaseNoteModal} - /> - )} - {showOfflineData && ( + {isWaitingForConsent ? ( + <GrdfWaitConsent /> + ) : ( <> - <FluidChart fluidType={fluidType} key={lastDataDateKey} /> - <ConsumptionDetails fluidType={fluidType} /> - {!isMulti && ( - <KonnectorViewerCard - fluidType={fluidType} - showOfflineData={true} - key={fluidType} - /> + {showOfflineData && ( + <> + <FluidChart fluidType={fluidType} key={lastDataDateKey} /> + <ConsumptionDetails fluidType={fluidType} /> + </> )} </> )} - {!showOfflineData && ( - <div className="konnector-section"> - {isMulti ? ( - <KonnectorViewerList /> - ) : ( - <KonnectorViewerCard - fluidType={fluidType} - showOfflineData={false} - /> - )} - </div> - )} + {!isMulti && <KonnectorViewerCard fluidType={fluidType} />} + + {isMulti && !showOfflineData && <KonnectorViewerList />} </Content> + + {/* MODALS */} + {openReleaseNoteModal && ( + <ReleaseNotesModal + open={openReleaseNoteModal} + handleCloseClick={handleCloseReleaseNoteModal} + /> + )} {/* Partner issue modals for individual fluids */} {fluidStatus .filter(fluid => fluid.maintenance) @@ -254,17 +266,15 @@ const ConsumptionView = ({ fluidType }: { fluidType: FluidType }) => { handleCloseClick={handleCloseCustomPopupModal} /> {Boolean(consentExpiredFluids.length) && - consentExpiredFluids.map(fluid => { - return ( - <ExpiredConsentModal - key={fluid} - open={openExpiredConsentModal} - handleCloseClick={() => setOpenExpiredConsentModal(false)} - fluidType={fluid} - toggleModal={() => setOpenExpiredConsentModal(prev => !prev)} - /> - ) - })} + consentExpiredFluids.map(fluid => ( + <ExpiredConsentModal + key={fluid} + open={openExpiredConsentModal} + handleCloseClick={() => setOpenExpiredConsentModal(false)} + fluidType={fluid} + toggleModal={() => setOpenExpiredConsentModal(prev => !prev)} + /> + ))} </> ) } diff --git a/src/components/Consumption/FluidButtons/FluidButton.tsx b/src/components/Consumption/FluidButtons/FluidButton.tsx index 3d71f623f..cd08a9944 100644 --- a/src/components/Consumption/FluidButtons/FluidButton.tsx +++ b/src/components/Consumption/FluidButtons/FluidButton.tsx @@ -33,7 +33,7 @@ const FluidButton = ({ fluidType, isActive }: FluidButtonProps) => { if ( (!isMulti && fluidStatus[fluidType].status === FluidState.ERROR) || (fluidType !== FluidType.WATER && - fluidStatus[fluidType].status === FluidState.ERROR_LOGIN_FAILED) + fluidStatus[fluidType].status === FluidState.LOGIN_FAILED) ) { return true } diff --git a/src/components/Konnector/ConnectionResult/ConnectionResult.tsx b/src/components/Konnector/ConnectionResult/ConnectionResult.tsx index 70062c459..323e24e4e 100644 --- a/src/components/Konnector/ConnectionResult/ConnectionResult.tsx +++ b/src/components/Konnector/ConnectionResult/ConnectionResult.tsx @@ -2,7 +2,6 @@ import Button from '@material-ui/core/Button' import warningDark from 'assets/icons/ico/warning-dark.svg' import warningWhite from 'assets/icons/ico/warning-white.svg' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' -import Loader from 'components/Loader/Loader' import { useClient } from 'cozy-client' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { FluidType, KonnectorUpdate } from 'enums' @@ -18,7 +17,7 @@ import { updateSgeStore, } from 'store/global/global.slice' import { useAppDispatch, useAppSelector } from 'store/hooks' -import { getFluidTypeTranslation, getKonnectorUpdateError } from 'utils/utils' +import { getKonnectorUpdateError } from 'utils/utils' import './connectionResult.scss' interface ConnectionResultProps { @@ -38,7 +37,6 @@ const ConnectionResult = ({ const account: Account | null = currentFluidStatus.connection.account const [deleting, setDeleting] = useState<boolean>(false) - const [updating, setUpdating] = useState<boolean>(false) const [lastExecutionDate, setLastExecutionDate] = useState<string | DateTime>( '-' ) @@ -47,7 +45,6 @@ const ConnectionResult = ({ const [outDatedDataDays, setOutDatedDataDays] = useState<number | null>(null) const updateKonnector = async () => { - setUpdating(true) setStatus('') setLastExecutionDate('-') setKonnectorError('') @@ -62,7 +59,6 @@ const ConnectionResult = ({ fluidConnection: updatedConnection, }) ) - setUpdating(false) } const deleteAccountsAndTriggers = useCallback(async () => { @@ -160,11 +156,21 @@ const ConnectionResult = ({ ) ) } - if (isOutdated()) { - setOutDatedDataDays(isOutdated()) - } + const outdated = isOutdated() + if (outdated) setOutDatedDataDays(outdated) }, [currentFluidStatus.connection.triggerState, isOutdated]) + const getFluidTypeTranslation = (fluidType: FluidType) => { + switch (fluidType) { + case FluidType.GAS: + return 'de gaz' + case FluidType.ELECTRICITY: + return "d'électricité" + default: + return "d'eau" + } + } + const consentError = konnectorError === KonnectorUpdate.ERROR_CONSENT_FORM_GAS || konnectorError === KonnectorUpdate.ERROR_UPDATE_OAUTH @@ -180,15 +186,12 @@ const ConnectionResult = ({ // First check if there is partner error from backoffice if (currentFluidStatus.maintenance) { return ( - <div className="connection-caption text-16-normal"> - <div className="text-16-normal"> - <div className="connection-caption"> - {t('konnector_form.wait_end_issue')} - </div> - </div> + <div className="connection-caption"> + {t('konnector_form.wait_end_issue')} </div> ) } + // Else check if konnector is in error state if (status === 'errored') { return ( @@ -220,46 +223,40 @@ const ConnectionResult = ({ ) } + const getErrorClassName = (): string => { + if ( + status === 'errored' && + !hasUpdatedToday() && + !currentFluidStatus.maintenance + ) { + return 'connection-update-errored' + } + return '' + } + return ( <div className="connection-update-result"> - <div - className={ - status === 'errored' && - !hasUpdatedToday() && - !currentFluidStatus.maintenance - ? 'connection-update-errored' - : '' - } - > - {getConnectionStatus()} - </div> + <div className={getErrorClassName()}>{getConnectionStatus()}</div> <div className="inline-buttons"> {!consentError && ( <Button aria-label={t('konnector_form.accessibility.button_disconnect')} onClick={deleteAccountsAndTriggers} - disabled={updating || deleting} + disabled={deleting} className="btnSecondary" > - {deleting - ? t('konnector_form.loading') - : t('konnector_form.button_disconnect')} + {t(`konnector_form.${deleting ? 'loading' : 'button_disconnect'}`)} </Button> )} <Button aria-label={t('konnector_form.accessibility.button_update')} onClick={consentError ? handleRefreshConsent : updateKonnector} - disabled={updating || deleting} + disabled={deleting} className="btnPrimary" > - {updating && <Loader color="black" />} - {!updating && ( - <div> - {consentError - ? t('konnector_form.button_oauth_reload') - : t('konnector_form.button_update')} - </div> - )} + {consentError + ? t('konnector_form.button_oauth_reload') + : t('konnector_form.button_update')} </Button> </div> </div> diff --git a/src/components/Konnector/KonnectorModal.spec.tsx b/src/components/Konnector/KonnectorModal.spec.tsx index cc72f6b71..306d498d7 100644 --- a/src/components/Konnector/KonnectorModal.spec.tsx +++ b/src/components/Konnector/KonnectorModal.spec.tsx @@ -23,7 +23,7 @@ describe('KonnectorModal component', () => { error={null} fluidType={FluidType.ELECTRICITY} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> @@ -41,7 +41,7 @@ describe('KonnectorModal component', () => { error={null} fluidType={FluidType.ELECTRICITY} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> @@ -61,7 +61,7 @@ describe('KonnectorModal component', () => { error={null} fluidType={FluidType.ELECTRICITY} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> @@ -80,18 +80,18 @@ describe('KonnectorModal component', () => { error={KonnectorError.LOGIN_FAILED} fluidType={FluidType.ELECTRICITY} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> </Provider> ) expect( - baseElement.getElementsByClassName('kce-picto-txt')[0] + baseElement.getElementsByClassName('headerError')[0] ).toBeInTheDocument() }) it('should render unknown error', async () => { - const { baseElement } = render( + render( <Provider store={store}> <KonnectorModal open={true} @@ -100,15 +100,13 @@ describe('KonnectorModal component', () => { error={null} fluidType={FluidType.ELECTRICITY} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> </Provider> ) - expect( - baseElement.getElementsByClassName('err-data-2')[0] - ).toBeInTheDocument() + expect(screen.getByText('konnector_modal.error_data_2')).toBeInTheDocument() }) it('should render update error', async () => { const { baseElement } = render( @@ -120,7 +118,7 @@ describe('KonnectorModal component', () => { error={null} fluidType={FluidType.WATER} handleCloseClick={mockHandleCloseClick} - isLogging={false} + isVerifyingIdentity={false} account={null} handleAccountDeletion={jest.fn()} /> diff --git a/src/components/Konnector/KonnectorModal.tsx b/src/components/Konnector/KonnectorModal.tsx index 800693207..5d89e4ee4 100644 --- a/src/components/Konnector/KonnectorModal.tsx +++ b/src/components/Konnector/KonnectorModal.tsx @@ -25,7 +25,7 @@ interface KonnectorModalProps { open: boolean isUpdating: boolean /** Only used for SGE when searching for user identity */ - isLogging: boolean + isVerifyingIdentity: boolean state: string | null error: KonnectorError | null fluidType: FluidType @@ -37,7 +37,7 @@ interface KonnectorModalProps { const KonnectorModal = ({ open, isUpdating, - isLogging, + isVerifyingIdentity, state, error, fluidType, @@ -48,17 +48,19 @@ const KonnectorModal = ({ const { t } = useI18n() const fluidName = getFluidName(fluidType) const [index, setIndex] = useState<number>(0) + /** Only used for enedis to see common errors */ + const [showCommonErrors, setShowCommonErrors] = useState(false) + const shuffledWaitingTexts = useMemo(() => { if (fluidType) { return shuffle(connectionWaitingText) - } else { - return connectionWaitingText } + return connectionWaitingText }, [fluidType]) + const firstConnectionWaitingTexts = firstConnectionWaitingText.concat( ...shuffledWaitingTexts ) - const [showCommonErrors, setShowCommonErrors] = useState(false) const getUpdatingText = useCallback(() => { return ( @@ -81,7 +83,7 @@ const KonnectorModal = ({ const getConnectionText = useCallback(() => { return ( <div className="kmodal-waiting-text text-18-italic"> - {isLogging ? ( + {isVerifyingIdentity ? ( <p className="text-18-white">{t('konnector_modal.logging_txt')}</p> ) : ( firstConnectionWaitingTexts.map((text, idx) => ( @@ -98,13 +100,13 @@ const KonnectorModal = ({ )} </div> ) - }, [firstConnectionWaitingTexts, index, isLogging, t]) + }, [firstConnectionWaitingTexts, index, isVerifyingIdentity, t]) /** Returns connection success contents, depending on the fluid and update status */ const connectionSuccessContent = () => ( <div className="konnector-config"> <Icon icon={successIcon} size={48} /> - <div className="kcs-picto-txt text-20-bold"> + <div className="headerSuccess text-20-bold"> {t(`konnector_modal.success_${isUpdating ? 'update_' : ''}txt`)} </div> <b> @@ -114,8 +116,8 @@ const KonnectorModal = ({ }${fluidName}` )} </b> - <p - style={{ fontWeight: 400 }} + <div + className="light" dangerouslySetInnerHTML={{ __html: t( `konnector_modal.success_data_additional_${ @@ -157,11 +159,13 @@ const KonnectorModal = ({ {t('konnector_modal.accessibility.window_title')} </div> <div className="kmodal-content"> - {open && !state ? ( + {/* NEITHER ERROR NOR SUCCESS => LOADING */} + {!state ? ( <> <Loader fluidType={fluidType} /> - {!isLogging && ( - <div className="kmodal-content-text kmodal-content-text-center text-16-normal"> + {!isVerifyingIdentity && ( + <div className="kmodal-content-text text-16-normal"> + {/* TODO remove kc-wait */} <div className="kc-wait text-16-bold"> {t( `konnector_modal.loading_data${isUpdating ? '_update' : ''}` @@ -174,6 +178,7 @@ const KonnectorModal = ({ </> ) : ( <> + {/* ERROR OR SUCCESS */} <div className="kmodal-info"> {state === ERROR_EVENT && ( <> @@ -181,7 +186,7 @@ const KonnectorModal = ({ // LOGIN FAILED FOR ENEDIS AND EGL <div className="konnector-config"> <Icon icon={errorIcon} size={48} /> - <div className="kce-picto-txt text-20-bold"> + <div className="headerError text-20-bold"> {t('konnector_modal.error_txt')} </div> <div> @@ -198,18 +203,17 @@ const KonnectorModal = ({ )} </div> )} - {/* Show common errors */} + {/* Show common errors for enedis */} {fluidType === FluidType.ELECTRICITY && ( <> - {!showCommonErrors && ( + {!showCommonErrors ? ( <Button - className="btnText commonErrors" + className="btnText" onClick={() => setShowCommonErrors(true)} > {t('konnector_modal.show_common_error')} </Button> - )} - {showCommonErrors && ( + ) : ( <div className="commonErrorsList" dangerouslySetInnerHTML={{ @@ -227,7 +231,7 @@ const KonnectorModal = ({ isUpdating && fluidType === FluidType.ELECTRICITY && ( // MISMATCH UPDATE ERROR ENEDIS - <div className="kce-picto-txt konnector-config mismatch"> + <div className="headerError konnector-config mismatch"> <Icon icon={EnedisIcon} width={120} height={80} /> <div className="title text-20-bold"> {t('konnector_modal.mismatch.title')} @@ -243,44 +247,40 @@ const KonnectorModal = ({ </div> </div> )} - {error === KonnectorError.CHALLENGE_ASKED && + {error === + KonnectorError.USER_ACTION_NEEDED_ACCOUNT_REMOVED && fluidType === FluidType.GAS && ( - // CONSENT FORM ERROR GRDF <div className="konnector-config"> <Icon icon={errorIcon} size={48} /> - <div className="kce-picto-txt text-20-bold"> + <div className="headerError text-20-bold"> {t('konnector_modal.error_txt')} </div> - <div className="title text-20-bold"> + <div className="title text-16-bold"> {t('konnector_modal.error_consent_form_gas_title')} </div> - <div className="err-data-2"> - {t('konnector_modal.error_consent_form_gas_content')} - </div> - <div className="err-data-2"> - {t( - 'konnector_modal.error_consent_form_gas_content_2' - )} + <div className="light text-14-regular"> + {t('konnector_modal.error_consent_form_gas_report')} </div> </div> )} {error !== KonnectorError.LOGIN_FAILED && error !== KonnectorError.TERMS_VERSION_MISMATCH && - error !== KonnectorError.CHALLENGE_ASKED && ( + error !== + KonnectorError.USER_ACTION_NEEDED_ACCOUNT_REMOVED && ( // DEFAULT CASE <div className="konnector-config"> <Icon icon={errorIcon} size={48} /> - <div className="kce-picto-txt text-20-bold"> + <div className="headerError text-20-bold"> {t('konnector_modal.error_txt')} </div> - <div> + <div className="text-15-bold"> {t( `konnector_modal.error_data_${ isUpdating ? 'update_' : '' }${fluidName}` )} </div> - <div className="err-data-2"> + <div className="text-14-regular"> {t('konnector_modal.error_data_2')} </div> </div> diff --git a/src/components/Konnector/KonnectorModalFooter.tsx b/src/components/Konnector/KonnectorModalFooter.tsx index dbc539dcf..2023ed972 100644 --- a/src/components/Konnector/KonnectorModalFooter.tsx +++ b/src/components/Konnector/KonnectorModalFooter.tsx @@ -1,9 +1,6 @@ import Button from '@material-ui/core/Button' import { useClient } from 'cozy-client' -import { - ERROR_EVENT, - SUCCESS_EVENT, -} from 'cozy-harvest-lib/dist/models/flowEvents' +import { SUCCESS_EVENT } from 'cozy-harvest-lib/dist/models/flowEvents' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { KonnectorError } from 'enums' import { Account } from 'models' @@ -12,6 +9,8 @@ import { useNavigate } from 'react-router-dom' import AccountService from 'services/account.service' import './konnectorModal.scss' +declare let __SAU_ISSUE_DIRECT_LINK__: string + interface KonnectorModalFooterProps { state: string | null error: KonnectorError | null @@ -32,6 +31,7 @@ const KonnectorModalFooter = ({ const { t } = useI18n() const client = useClient() const navigate = useNavigate() + const handleSGELoginRetry = useCallback(() => { handleCloseClick(state === SUCCESS_EVENT) navigate('/connect/electricity') @@ -46,73 +46,87 @@ const KonnectorModalFooter = ({ } }, [account, client, handleAccountDeletion, navigate]) - const defaultButton = ( - <Button - aria-label={t('konnector_modal.accessibility.button_close')} - onClick={() => handleCloseClick(state === SUCCESS_EVENT)} - className="btnPrimary" - > - <div>{t('konnector_modal.button_validate')}</div> - </Button> - ) - - const errorButtons = () => { - switch (error) { - case KonnectorError.USER_ACTION_NEEDED: - // INSEE CODE ERROR ENEDIS - return ( - <Button - aria-label={t('konnector_modal.accessibility.button_close')} - onClick={() => handleCloseClick(state === SUCCESS_EVENT)} - className="btnPrimary" - > - <div>{t('konnector_modal.button_understood')}</div> - </Button> - ) - case KonnectorError.LOGIN_FAILED: - case KonnectorError.CHALLENGE_ASKED: - // INCOMPLETE CONSENT FORM - GRDF - return ( - <Button - aria-label={t('konnector_modal.accessibility.button_close')} - onClick={() => handleCloseClick(state === SUCCESS_EVENT)} - className="btnPrimary" - > - <div>{t('konnector_modal.button_try_again')}</div> - </Button> - ) - case KonnectorError.TERMS_VERSION_MISMATCH: - return ( - <div className="buttons"> - <Button - aria-label={t('konnector_modal.accessibility.button_close')} - onClick={() => handleCloseClick(state === SUCCESS_EVENT)} - className="btnSecondary" - > - <div>{t('konnector_modal.button_later')}</div> - </Button> - <Button - aria-label={t('konnector_modal.accessibility.button_close')} - onClick={ - !isUpdating ? handleSGELoginRetry : handleResetSGEAccount - } - className="btnPrimary" - > - <div> - {!isUpdating - ? t('konnector_modal.button_check_info') - : t('konnector_modal.button_go')} - </div> - </Button> + if (error === KonnectorError.USER_ACTION_NEEDED) { + // INSEE CODE ERROR ENEDIS + return ( + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} + className="btnPrimary" + > + <div>{t('konnector_modal.button_understood')}</div> + </Button> + ) + } else if (error === KonnectorError.LOGIN_FAILED) { + // INCOMPLETE CONSENT FORM - GRDF // what is this comment ? + return ( + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} + className="btnPrimary" + > + <div>{t('konnector_modal.button_try_again')}</div> + </Button> + ) + } else if (error === KonnectorError.CHALLENGE_ASKED) { + return ( + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} + className="btnPrimary" + > + <div>{t('konnector_modal.button_come_back_later')}</div> + </Button> + ) + } else if (error === KonnectorError.TERMS_VERSION_MISMATCH) { + return ( + <div className="buttons"> + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} + className="btnSecondary" + > + <div>{t('konnector_modal.button_later')}</div> + </Button> + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={!isUpdating ? handleSGELoginRetry : handleResetSGEAccount} + className="btnPrimary" + > + <div> + {!isUpdating + ? t('konnector_modal.button_check_info') + : t('konnector_modal.button_go')} </div> - ) - default: - // DEFAULT FOOTER BUTTONS - return defaultButton - } + </Button> + </div> + ) + } else if (error === KonnectorError.USER_ACTION_NEEDED_ACCOUNT_REMOVED) { + return ( + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => { + window.open( + `${__SAU_ISSUE_DIRECT_LINK__}?version=${client.appMetadata.version}` + ) + handleCloseClick(state === SUCCESS_EVENT) + }} + className="btnPrimary" + > + <div>{t('konnector_modal.button_contact')}</div> + </Button> + ) + } else { + return ( + <Button + aria-label={t('konnector_modal.accessibility.button_close')} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} + className="btnPrimary" + > + <div>{t('konnector_modal.button_validate')}</div> + </Button> + ) } - - return <>{state === ERROR_EVENT ? errorButtons() : defaultButton}</> } export default KonnectorModalFooter diff --git a/src/components/Konnector/KonnectorViewerCard.tsx b/src/components/Konnector/KonnectorViewerCard.tsx index fcbdb43fb..4b202499a 100644 --- a/src/components/Konnector/KonnectorViewerCard.tsx +++ b/src/components/Konnector/KonnectorViewerCard.tsx @@ -12,7 +12,6 @@ import OfflinePicto from 'assets/icons/visu/offline-param.svg' import classNames from 'classnames' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' import Connection from 'components/Connection/Connection' -import { GrdfWaitConsent } from 'components/Connection/GRDFConnect/GrdfWaitConsent' import KonnectorModal from 'components/Konnector/KonnectorModal' import { useClient } from 'cozy-client' import { isKonnectorRunning } from 'cozy-harvest-lib/dist/helpers/triggers' @@ -54,27 +53,24 @@ import { updateFluidConnection, } from 'store/global/global.slice' import { useAppDispatch, useAppSelector } from 'store/hooks' +import logApp from 'utils/logger' import { getParamPicto } from 'utils/picto' import { getKonnectorSlug } from 'utils/utils' import ConnectionNotFound from './ConnectionNotFound/ConnectionNotFound' import ConnectionResult from './ConnectionResult/ConnectionResult' import './konnectorViewerCard.scss' -interface KonnectorViewerCardProps { - showOfflineData: boolean - fluidType: Exclude<FluidType, FluidType.MULTIFLUID> -} - const KonnectorViewerCard = ({ - showOfflineData, fluidType, -}: KonnectorViewerCardProps) => { +}: { + fluidType: Exclude<FluidType, FluidType.MULTIFLUID> +}) => { const { t } = useI18n() const client = useClient() const navigate = useNavigate() const dispatch = useAppDispatch() const { - chart: { showConnectionDetails }, + chart: { showConnectionDetails, showOfflineData }, challenge: { currentChallenge }, global: { fluidStatus, shouldRefreshConsent, partnersInfo }, } = useAppSelector(state => state.ecolyo) @@ -83,25 +79,27 @@ const KonnectorViewerCard = ({ const fluidState = currentFluidStatus.status const { konnector, account, trigger } = currentFluidStatus.connection const currentFluidName = FluidType[currentFluidStatus.fluidType].toLowerCase() + const [openModal, setOpenModal] = useState(false) const [isUpdating, setIsUpdating] = useState(false) - const [isLogging, setIsLogging] = useState( + const [isVerifyingIdentity, setIsVerifyingIdentity] = useState( fluidType === FluidType.ELECTRICITY ) const [konnectorErrorDescription, setKonnectorErrorDescription] = useState<KonnectorError | null>(null) const [konnectorState, setKonnectorState] = useState<string | null>(null) const [isOutdatedData, setIsOutdatedData] = useState<number | null>(null) + + const isWaitingForConsent = + fluidType === FluidType.GAS && + currentFluidStatus.status === FluidState.CHALLENGE_ASKED + const fluidService = useMemo(() => new FluidService(client), [client]) const partnersInfoService = useMemo( () => new PartnersInfoService(client), [client] ) - const lastDataDate = currentFluidStatus.lastDataDate - ? currentFluidStatus.lastDataDate.toLocaleString() + fluidType - : fluidType - const iconType = getParamPicto(currentFluidStatus.fluidType) const toggleAccordion = () => { @@ -111,19 +109,19 @@ const KonnectorViewerCard = ({ const updateGlobalFluidStatus = useCallback(async (): Promise< FluidStatus[] > => { - const _updatedFluidStatus = await fluidService.getFluidStatus() + const updatedFluidStatus = await fluidService.getFluidStatus() const refDate = DateTime.fromISO('0001-01-01') - let _lastDataDate = DateTime.fromISO('0001-01-01') - for (const fluid of _updatedFluidStatus) { - if (fluid?.lastDataDate && fluid?.lastDataDate > _lastDataDate) { - _lastDataDate = fluid.lastDataDate + let lastDataDate = DateTime.fromISO('0001-01-01') + for (const fluid of updatedFluidStatus) { + if (fluid?.lastDataDate && fluid?.lastDataDate > lastDataDate) { + lastDataDate = fluid.lastDataDate } } - if (_lastDataDate > refDate) { - dispatch(setSelectedDate(_lastDataDate)) + if (lastDataDate > refDate) { + dispatch(setSelectedDate(lastDataDate)) } - return _updatedFluidStatus + return updatedFluidStatus }, [dispatch, fluidService]) const refreshChallengeState = useCallback(async () => { @@ -172,9 +170,10 @@ const KonnectorViewerCard = ({ const isGlobalLoginFailed = konnectorErrorDescription === KonnectorError.LOGIN_FAILED || konnectorErrorDescription === KonnectorError.UNKNOWN_ERROR || - konnectorErrorDescription === KonnectorError.CHALLENGE_ASKED || konnectorErrorDescription === KonnectorError.CRITICAL || - konnectorErrorDescription === KonnectorError.MISSING_SECRET + konnectorErrorDescription === KonnectorError.MISSING_SECRET || + konnectorErrorDescription === + KonnectorError.USER_ACTION_NEEDED_ACCOUNT_REMOVED // CASE FOR ENEDIS CODE INSEE ERROR const isEnedisCodeInseeError = @@ -188,25 +187,21 @@ const KonnectorViewerCard = ({ (isGlobalLoginFailed || isEnedisCodeInseeError) if (shouldDeleteAccount) { + logApp('info', `shouldDeleteAccount`) // KEEP LAST LOGIN FOR EPGL - let lastEpglLogin = '' if ( fluidSlug === FluidSlugType.WATER && currentFluidStatus.connection.account?.auth ) { const auth = currentFluidStatus.connection.account .auth as AccountEGLData - lastEpglLogin = auth.login + const lastEpglLogin = auth.login + dispatch(setLastEpglLogin(lastEpglLogin)) } // DELETE ACCOUNT const accountService = new AccountService(client) await accountService.deleteAccount(account) await handleAccountDeletion() - - // RESTORE LAST KNOWN CREDENTIALS - if (lastEpglLogin) { - dispatch(setLastEpglLogin(lastEpglLogin)) - } } else { const updatedFluidStatus = await fluidService.getFluidStatus(partnersInfo) @@ -248,12 +243,6 @@ const KonnectorViewerCard = ({ }, [dispatch, fluidType, navigate]) const getConnectionCard = useCallback(() => { - if ( - fluidType === FluidType.GAS && - fluidState === FluidState.CHALLENGE_ASKED - ) { - return <GrdfWaitConsent /> - } if (showOfflineData && !account) { return ( <Button className="btnPrimary" onClick={toggleModalConnection}> @@ -261,35 +250,32 @@ const KonnectorViewerCard = ({ </Button> ) } + if (fluidState === FluidState.KONNECTOR_NOT_FOUND && !isUpdating) { return <ConnectionNotFound konnectorSlug={fluidSlug} /> } + // Handle login failed for EGL - else if ( + if ( (fluidType === FluidType.WATER && - fluidState === FluidState.ERROR_LOGIN_FAILED) || + fluidState === FluidState.LOGIN_FAILED) || (account && currentFluidStatus.status !== FluidState.NOT_CONNECTED) ) { return ( <ConnectionResult handleAccountDeletion={handleAccountDeletion} fluidType={fluidType} - key={lastDataDate} /> ) - } else { - return <Connection fluidType={currentFluidStatus.fluidType} /> } }, [ account, - currentFluidStatus.fluidType, currentFluidStatus.status, fluidSlug, fluidState, fluidType, handleAccountDeletion, isUpdating, - lastDataDate, showOfflineData, t, toggleModalConnection, @@ -324,59 +310,34 @@ const KonnectorViewerCard = ({ ] ) - const getIconForStatus = ( - status: FluidState, - maintenance: boolean, - connection: FluidConnection, - outdatedData: number | null - ) => { - if (maintenance) { - return ( - <StyledIcon - icon={PartnersIssueNotif} - size={24} - className="konnector-state-picto" - /> - ) - } + /** Display konnector icon & its smaller status icon in upper left corner */ + const displayKonnectorIcon = useCallback(() => { + const { maintenance, status, connection } = currentFluidStatus + let statusIcon = null - if ( - (status === FluidState.ERROR || - status === FluidState.ERROR_LOGIN_FAILED) && + if (maintenance) { + statusIcon = PartnersIssueNotif + } else if ( + (status === FluidState.ERROR || status === FluidState.LOGIN_FAILED) && connection.account ) { - return ( - <StyledIcon - icon={ErrorNotif} - size={24} - className="konnector-state-picto" - /> - ) + statusIcon = ErrorNotif + } else if (isOutdatedData && isOutdatedData > 0) { + statusIcon = WarningNotif } - if (outdatedData && outdatedData > 0) { - return ( - <StyledIcon - icon={WarningNotif} - size={24} - className="konnector-state-picto" - /> - ) - } - } - - const displayKonnectorIcon = useCallback(() => { return ( <div className="konnector-icon"> <Icon icon={currentFluidStatus.connection.account ? iconType : OfflinePicto} size={49} /> - {getIconForStatus( - currentFluidStatus.status, - currentFluidStatus.maintenance, - currentFluidStatus.connection, - isOutdatedData + {statusIcon && ( + <StyledIcon + icon={statusIcon} + size={24} + className="konnector-state-picto" + /> )} </div> ) @@ -451,7 +412,7 @@ const KonnectorViewerCard = ({ callbackResponse(ERROR_EVENT) }) connectionFlow.jobWatcher.on(LOGIN_SUCCESS_EVENT, () => { - setIsLogging(false) + setIsVerifyingIdentity(false) }) connectionFlow.jobWatcher.on(SUCCESS_EVENT, () => { callbackResponse(SUCCESS_EVENT) @@ -485,21 +446,18 @@ const KonnectorViewerCard = ({ return ( <div className="konnector-section-root"> - {!showOfflineData && ( - <AccordionDetails>{getConnectionCard()}</AccordionDetails> - )} - {showOfflineData && ( + {!showOfflineData && <Connection fluidType={fluidType} />} + {showOfflineData && !isWaitingForConsent && ( <Accordion expanded={showConnectionDetails} onChange={toggleAccordion} classes={{ - root: `expansion-panel-root ${ - !currentFluidStatus.maintenance && - (currentFluidStatus.status === FluidState.ERROR || - currentFluidStatus.status === FluidState.ERROR_LOGIN_FAILED) - ? 'red-border' - : '' - }`, + root: classNames('expansion-panel-root', { + ['red-border']: + !currentFluidStatus.maintenance && + (currentFluidStatus.status === FluidState.ERROR || + currentFluidStatus.status === FluidState.LOGIN_FAILED), + }), }} > <AccordionSummary @@ -537,10 +495,10 @@ const KonnectorViewerCard = ({ <KonnectorModal open={openModal} isUpdating={isUpdating} - isLogging={isLogging} + isVerifyingIdentity={isVerifyingIdentity} state={konnectorState} error={konnectorErrorDescription} - fluidType={currentFluidStatus.fluidType} + fluidType={fluidType} handleCloseClick={handleConnectionEnd} handleAccountDeletion={handleAccountDeletion} account={account} diff --git a/src/components/Konnector/KonnectorViewerList.tsx b/src/components/Konnector/KonnectorViewerList.tsx index c5d4eac40..152481df0 100644 --- a/src/components/Konnector/KonnectorViewerList.tsx +++ b/src/components/Konnector/KonnectorViewerList.tsx @@ -11,8 +11,9 @@ import './konnectorViewerCard.scss' const KonnectorViewerList = () => { const { t } = useI18n() - const { fluidStatus } = useAppSelector(state => state.ecolyo.global) const navigate = useNavigate() + const { fluidStatus } = useAppSelector(state => state.ecolyo.global) + const goToFluid = (fluidType: FluidType) => { navigate(`/consumption/${getFluidName(fluidType)}`) } diff --git a/src/components/Konnector/__snapshots__/KonnectorModal.spec.tsx.snap b/src/components/Konnector/__snapshots__/KonnectorModal.spec.tsx.snap index e4f491c98..26f5020cd 100644 --- a/src/components/Konnector/__snapshots__/KonnectorModal.spec.tsx.snap +++ b/src/components/Konnector/__snapshots__/KonnectorModal.spec.tsx.snap @@ -62,7 +62,7 @@ exports[`KonnectorModal component should be rendered correctly 1`] = ` </div> </div> <div - class="kmodal-content-text kmodal-content-text-center text-16-normal" + class="kmodal-content-text text-16-normal" > <div class="kc-wait text-16-bold" diff --git a/src/components/Konnector/konnectorModal.scss b/src/components/Konnector/konnectorModal.scss index 1c2777496..fdbaab03c 100644 --- a/src/components/Konnector/konnectorModal.scss +++ b/src/components/Konnector/konnectorModal.scss @@ -38,16 +38,14 @@ } } - .kmodal-content-text-center { - text-align: center; - } - .kmodal-info { padding: 1rem; - text-align: center; + display: flex; + flex-direction: column; + gap: 24px; .buttons { display: flex; - gap: 0.825rem; + gap: 1rem; } .konnector-config { align-items: center; @@ -58,32 +56,22 @@ gap: 1rem; .elec-fail { color: $grey-bright; - margin-top: 1rem; } &.mismatch { .title { color: $orange; } - div { - margin-bottom: 1rem; - } .info { color: $grey-bright; } } - .kce-picto-txt { - color: $red-primary; - } - - .kcs-picto-txt { + .headerSuccess { color: $multi-color; } - - .commonErrors { - text-decoration: underline; - cursor: pointer; - margin: 1rem auto 0.5rem; + .headerError { + color: $red-primary; } + .commonErrorsList { text-align: left; span { @@ -104,10 +92,10 @@ } } } + } - button { - margin-top: 1rem; - } + .light { + color: $grey-bright; } } diff --git a/src/components/Options/ReportOptions/ReportOptions.tsx b/src/components/Options/ReportOptions/ReportOptions.tsx index 528aa2951..ab9bf280a 100644 --- a/src/components/Options/ReportOptions/ReportOptions.tsx +++ b/src/components/Options/ReportOptions/ReportOptions.tsx @@ -35,7 +35,7 @@ const ReportOptions = () => { const isWaterConnected = fluidStatus[FluidType.WATER].status !== FluidState.NOT_CONNECTED && fluidStatus[FluidType.WATER].status !== FluidState.KONNECTOR_NOT_FOUND && - fluidStatus[FluidType.WATER].status !== FluidState.ERROR_LOGIN_FAILED + fluidStatus[FluidType.WATER].status !== FluidState.LOGIN_FAILED const setWaterLimit = ( e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element> diff --git a/src/enums/fluid.enum.ts b/src/enums/fluid.enum.ts index 4f6608f2c..e0d99c964 100644 --- a/src/enums/fluid.enum.ts +++ b/src/enums/fluid.enum.ts @@ -6,10 +6,10 @@ export enum FluidType { } export enum FluidState { - KONNECTOR_NOT_FOUND = 0, - NOT_CONNECTED = 1, - DONE = 200, - ERROR = 300, - ERROR_LOGIN_FAILED = 301, - CHALLENGE_ASKED = 400, + KONNECTOR_NOT_FOUND = 'KONNECTOR_NOT_FOUND', + NOT_CONNECTED = 'NOT_CONNECTED', + DONE = 'DONE', + ERROR = 'ERROR', + LOGIN_FAILED = 'LOGIN_FAILED', + CHALLENGE_ASKED = 'CHALLENGE_ASKED', } diff --git a/src/enums/konnectorStatus.enum.ts b/src/enums/konnectorStatus.enum.ts index 4e8543316..ca1975dae 100644 --- a/src/enums/konnectorStatus.enum.ts +++ b/src/enums/konnectorStatus.enum.ts @@ -6,11 +6,11 @@ export enum KonnectorError { UNKNOWN_ERROR = 'UNKNOWN_ERROR', CRITICAL = 'exit status 1', MISSING_SECRET = "Cannot read property 'secret' of null", + USER_ACTION_NEEDED_ACCOUNT_REMOVED = 'USER_ACTION_NEEDED.ACCOUNT_REMOVED', } export enum KonnectorUpdate { ERROR_UPDATE = 'error_update', ERROR_UPDATE_OAUTH = 'error_update_oauth', LOGIN_FAILED = 'login_failed', - ERROR_CONSENT_FORM_GAS = 'error_consent_form_gas', } diff --git a/src/locales/fr.json b/src/locales/fr.json index f784635fc..5e3a72d8c 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -217,7 +217,8 @@ "consentCheck1": "Je consens à partager les données personnelles ci-dessus pour une durée d'<span>un\u00a0an</span>", "consentCheck2": "J’atteste être le titulaire du point de livraison (PCE) renseigné à l’étape précédente", "waiting": { - "mailSent": "Un mail va vous être envoyé par GRDF sur l’adresse mail :<br><span>%{email}</span>", + "mailSent": "Un mail vous a été envoyé...", + "mailDelay": "Patience, cela peut prendre jusqu'à 15 minutes", "validate": "Merci de valider l'autorisation d'accès à vos données", "comeback": "Une fois ce clic effectué, revenez ici pour accéder à vos données", "button_done": "C’est fait !" @@ -766,7 +767,6 @@ "error_login_failed": "Identifiants invalides", "error_update": "Un problème est survenu lors du rapatriement de vos données.", "error_update_oauth": "Votre autorisation pour afficher vos données %{fluid} a expiré.", - "error_consent_form_gas": "Vos données ne peuvent être récupérées car vous n'avez pas coché l'autorisation d'accès aux données informatives lors de votre partage de consentement.", "button_oauth_reload": "Redonner mon consentement", "OK": "Ok", "konnector_delta": { @@ -821,18 +821,17 @@ "text2": "Reconfigurer mon connecteur\u00a0?", "text3": "La reconfiguration de votre connecteur passe par sa suppression et sa nouvelle installation. Vos données seront conservées." }, - "error_data_electricity": "Un problème est survenu. Vos données de consommation d’électricité ne seront pas chargées.", - "error_data_water": "Un problème est survenu. Vos données de consommation d’eau ne seront pas chargées.", "error_credentials_water": "Une erreur s'est glissée dans vos identifiants de connexion. Veuillez vérifier ces éléments et tenter de vous reconnecter. L'identifiant est un numéro à 7 chiffres (différent de votre numéro de contrat).", "error_credentials_electricity": "Il semblerait que les nom(s) et adresse ne concordent pas avec le numéro de votre compteur.", "error_credentials_electricity_2": "Nous ne pouvons vous donner accès aux données de consommation.", "error_credentials_update_water": "Une erreur s'est glissée dans vos identifiants de connexion. Veuillez vérifier ces éléments et tenter de vous reconnecter.", "error_credentials_update_electricity": "Un problème a lieu lors de la récupération de vos données. Merci de supprimer votre connecteur et vous reconnecter.", "error_credentials_update_gas": "Un problème a lieu lors de la récupération de vos données. Merci de supprimer votre connecteur et vous reconnecter.", - "error_data_gas": "Un problème est survenu. Vos données de consommation de gaz ne seront pas chargées.", - "error_consent_form_gas_title": "Nous n'avons pas pu connecter vos données de consommation de gaz à Ecolyo.", - "error_consent_form_gas_content": "En effet, le partage de vos données de consommation de gaz \"informatives\" doit être accepté.", - "error_consent_form_gas_content_2": "Merci de cocher \"OUI\" au partage de vos données de consommation de gaz, et à \"Autoriser l'accès à mes données informatives\".", + "error_consent_form_gas_title": "L'accès à vos données a été bloqué par GRDF suite à un consentement précédemment supprimé par vos soins.", + "error_consent_form_gas_report": "Merci de nous signaler le problème.", + "error_data_electricity": "Un problème est survenu. Vos données de consommation d’électricité ne seront pas chargées.", + "error_data_water": "Un problème est survenu. Vos données de consommation d’eau ne seront pas chargées.", + "error_data_gas": "Il semblerait que le service de connexion à vos données de gaz soit momentanément en panne.", "error_data_update_electricity": "Un problème est survenu. Vos données de consommation d’électricité n’ont pas été mises à jour.", "error_data_update_water": "Un problème est survenu. Vos données de consommation d’eau n’ont pas été mises à jour.", "error_data_update_gas": "Un problème est survenu. Vos données de consommation de gaz n’ont pas été mises à jour.", @@ -840,9 +839,11 @@ "button_validate": "Ok", "button_understood": "J'ai compris", "button_try_again": "Réessayer", + "button_contact": "Nous contacter", "button_check_info": "Vérifier les infos", "button_go": "J'y vais", "button_later": "Plus tard", + "button_come_back_later": "Revenir plus tard", "show_common_error": "Voir les erreurs récurrentes", "show_common_error_list": "<span>Le problème peut provenir des cas suivants :</span><ul><li>Vous avez un co-titulaire sur votre contrat. Veillez à bien entrer le nom du <span class=\"gold\">titulaire du contrat</span> et non le co-titulaire.</li><li> Votre nom comporte un tiret\u00a0? Tentez sans le tiret.</li><li>Entrez bien le nom de votre commune de résidence en entier (tirets et accents inclus)</li><li>Avez-vous bien entré le <span class=\"gold\">numéro de votre compteur</span> (PDL)\u00a0? Tout autre numéro (de contrat, de client) ne fonctionne pas.</li></ul><p>Si vous rencontrez toujours des difficultés, contactez notre service d'aide </p><div class=\"center\">Avez-vous pensez à vérifier ces informations\u00a0?</div>", "accessibility": { @@ -853,15 +854,15 @@ "consent_outdated": { "title": { "0": "Votre autorisation pour afficher vos données d’électricité a expiré", - "2": "Votre autorisation pour afficher vos données de gaz a expiré" + "2": "Aie !" }, "text1": { "0": "Veuillez re-donner votre consentement pour la transmission et la reconnexion de vos données ENEDIS à Ecolyo.", - "2": "Veuillez re-donner votre accord pour que GRDF nous transmette vos données de consommation." + "2": "L'accès à vos données de consommation de gaz a expiré." }, "text2": { "0": "Souhaitez-vous renouveler votre accord dès maintenant pour un an\u00a0?", - "2": "Voulez-vous donner votre accord sur votre compte GRDF maintenant\u00a0?" + "2": "Merci de redonner votre consentement pour y accéder." }, "later": "Plus tard", "go": "J'y vais", diff --git a/src/services/fluid.service.ts b/src/services/fluid.service.ts index 680364878..2d4a24fc3 100644 --- a/src/services/fluid.service.ts +++ b/src/services/fluid.service.ts @@ -1,13 +1,6 @@ import { Client } from 'cozy-client' import { FluidState, FluidType } from 'enums' -import { - Account, - FluidStatus, - Konnector, - PartnersInfo, - Trigger, - TriggerState, -} from 'models' +import { FluidStatus, Konnector, PartnersInfo, TriggerState } from 'models' import AccountService from 'services/account.service' import ConsumptionService from 'services/consumption.service' import ConfigService from 'services/fluidConfig.service' @@ -33,9 +26,14 @@ export default class FluidService { case 'done': return FluidState.DONE case 'errored': - if (state?.last_error === 'LOGIN_FAILED') - return FluidState.ERROR_LOGIN_FAILED - else return FluidState.ERROR + if (state?.last_error === 'LOGIN_FAILED') { + return FluidState.LOGIN_FAILED + } + if (state?.last_error === 'CHALLENGE_ASKED') { + return FluidState.CHALLENGE_ASKED + } + return FluidState.ERROR + default: return FluidState.NOT_CONNECTED } @@ -59,51 +57,47 @@ export default class FluidService { ): Promise<FluidStatus[]> => { const fluidConfig = new ConfigService().getFluidConfig() const accountService = new AccountService(this._client) - const [elecAccount, waterAccount, gasAccount]: (Account | null)[] = - await Promise.all([ - accountService.getAccountByType( - fluidConfig[FluidType.ELECTRICITY].konnectorConfig.slug - ), - accountService.getAccountByType( - fluidConfig[FluidType.WATER].konnectorConfig.slug - ), - accountService.getAccountByType( - fluidConfig[FluidType.GAS].konnectorConfig.slug - ), - ]) + const [elecAccount, waterAccount, gasAccount] = await Promise.all([ + accountService.getAccountByType( + fluidConfig[FluidType.ELECTRICITY].konnectorConfig.slug + ), + accountService.getAccountByType( + fluidConfig[FluidType.WATER].konnectorConfig.slug + ), + accountService.getAccountByType( + fluidConfig[FluidType.GAS].konnectorConfig.slug + ), + ]) const konnectorService = new KonnectorService(this._client) - const [elecKonnector, waterKonnector, gasKonnector]: (Konnector | null)[] = - await Promise.all([ - konnectorService.getKonnector( - fluidConfig[FluidType.ELECTRICITY].konnectorConfig.slug - ), - konnectorService.getKonnector( - fluidConfig[FluidType.WATER].konnectorConfig.slug - ), - konnectorService.getKonnector( - fluidConfig[FluidType.GAS].konnectorConfig.slug - ), - ]) + const [elecKonnector, waterKonnector, gasKonnector] = await Promise.all([ + konnectorService.getKonnector( + fluidConfig[FluidType.ELECTRICITY].konnectorConfig.slug + ), + konnectorService.getKonnector( + fluidConfig[FluidType.WATER].konnectorConfig.slug + ), + konnectorService.getKonnector( + fluidConfig[FluidType.GAS].konnectorConfig.slug + ), + ]) const triggerService = new TriggerService(this._client) - const [elecTrigger, waterTrigger, gasTrigger]: (Trigger | null)[] = - await Promise.all([ - elecAccount && elecKonnector - ? triggerService.getTrigger(elecAccount, elecKonnector) - : null, - waterAccount && waterKonnector - ? triggerService.getTrigger(waterAccount, waterKonnector) - : null, - gasAccount && gasKonnector - ? triggerService.getTrigger(gasAccount, gasKonnector) - : null, - ]) + const [elecTrigger, waterTrigger, gasTrigger] = await Promise.all([ + elecAccount && elecKonnector + ? triggerService.getTrigger(elecAccount, elecKonnector) + : null, + waterAccount && waterKonnector + ? triggerService.getTrigger(waterAccount, waterKonnector) + : null, + gasAccount && gasKonnector + ? triggerService.getTrigger(gasAccount, gasKonnector) + : null, + ]) const consumptionService = new ConsumptionService(this._client) - const [elecStatus, waterStatus, gasStatus]: (TriggerState | null)[] = - await Promise.all([ - elecTrigger ? triggerService.fetchTriggerState(elecTrigger) : null, - waterTrigger ? triggerService.fetchTriggerState(waterTrigger) : null, - gasTrigger ? triggerService.fetchTriggerState(gasTrigger) : null, - ]) + const [elecStatus, waterStatus, gasStatus] = await Promise.all([ + elecTrigger ? triggerService.fetchTriggerState(elecTrigger) : null, + waterTrigger ? triggerService.fetchTriggerState(waterTrigger) : null, + gasTrigger ? triggerService.fetchTriggerState(gasTrigger) : null, + ]) console.log('🚀 ~ FluidService ~ gasStatus:', gasStatus) const firstDataDates = await consumptionService.fetchAllFirstDateData(allFluids) diff --git a/src/store/global/global.slice.spec.ts b/src/store/global/global.slice.spec.ts index 494fe6fc6..3df92c909 100644 --- a/src/store/global/global.slice.spec.ts +++ b/src/store/global/global.slice.spec.ts @@ -284,7 +284,7 @@ describe('globalSlice', () => { firstDataDate: null, lastDataDate: null, maintenance: false, - status: 0, + status: FluidState.KONNECTOR_NOT_FOUND, connection: { shouldLaunchKonnector: true, isUpdating: true, diff --git a/src/store/global/global.slice.ts b/src/store/global/global.slice.ts index cd34d3130..f93aada9e 100644 --- a/src/store/global/global.slice.ts +++ b/src/store/global/global.slice.ts @@ -139,9 +139,9 @@ const getFluidTypesFromStatus = (fluidStatus: FluidStatus[]): FluidType[] => { if ( (fluid.status !== FluidState.KONNECTOR_NOT_FOUND && fluid.status !== FluidState.NOT_CONNECTED && - fluid.status !== FluidState.ERROR_LOGIN_FAILED) || + fluid.status !== FluidState.LOGIN_FAILED) || // Handle Login Error case for oauth konnectors - (fluid.status === FluidState.ERROR_LOGIN_FAILED && + (fluid.status === FluidState.LOGIN_FAILED && fluid.fluidType !== FluidType.WATER) ) { fluidTypes.push(fluid.fluidType) @@ -204,6 +204,7 @@ export const globalSlice = createSlice({ ) => { state.fluidStatus[fluidType].connection = fluidConnection }, + /** Restore last known credentials */ setLastEpglLogin: (state, action: PayloadAction<string>) => { state.lastEpglLogin = action.payload }, diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts index 79318b817..e36db9b7b 100644 --- a/src/utils/utils.spec.ts +++ b/src/utils/utils.spec.ts @@ -60,9 +60,9 @@ describe('utils test', () => { const result = getKonnectorUpdateError('LOGIN_FAILED') expect(result).toBe(KonnectorUpdate.LOGIN_FAILED) }) - it('should return KonnectorUpdate.ERROR_CONSENT_FORM_GAS for CHALLENGE_ASKED', () => { + it('should return KonnectorUpdate.ERROR_UPDATE for CHALLENGE_ASKED', () => { const result = getKonnectorUpdateError('CHALLENGE_ASKED') - expect(result).toBe(KonnectorUpdate.ERROR_CONSENT_FORM_GAS) + expect(result).toBe(KonnectorUpdate.ERROR_UPDATE) }) it('should return KonnectorUpdate.ERROR_UPDATE for an unknown type', () => { const result = getKonnectorUpdateError('UNKNOWN_TYPE') @@ -115,7 +115,7 @@ describe('utils test', () => { fluidType: FluidType.ELECTRICITY, }, { status: FluidState.ERROR, fluidType: FluidType.WATER }, - { status: FluidState.ERROR_LOGIN_FAILED, fluidType: FluidType.GAS }, + { status: FluidState.LOGIN_FAILED, fluidType: FluidType.GAS }, ] as FluidStatus[] expect(isKonnectorActive(fluidStatus, FluidType.ELECTRICITY)).toBe(true) expect(isKonnectorActive(fluidStatus, FluidType.GAS)).toBe(true) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ddf4b505a..fc5bf7ede 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -67,11 +67,10 @@ export const getPartnerKey = (fluidType: FluidType) => { export function getKonnectorUpdateError(type: string) { switch (type.toUpperCase()) { case 'USER_ACTION_NEEDED.OAUTH_OUTDATED': + case 'USER_ACTION_NEEDED.SCA_REQUIRED': return KonnectorUpdate.ERROR_UPDATE_OAUTH case 'LOGIN_FAILED': return KonnectorUpdate.LOGIN_FAILED - case 'CHALLENGE_ASKED': - return KonnectorUpdate.ERROR_CONSENT_FORM_GAS default: return KonnectorUpdate.ERROR_UPDATE } diff --git a/tests/__mocks__/fluidStatusData.mock.ts b/tests/__mocks__/fluidStatusData.mock.ts index ddb5ab5f4..e7762e79f 100644 --- a/tests/__mocks__/fluidStatusData.mock.ts +++ b/tests/__mocks__/fluidStatusData.mock.ts @@ -252,7 +252,7 @@ export const mockExpiredElec: FluidStatus = { export const mockExpiredGas: FluidStatus = { fluidType: FluidType.GAS, - status: FluidState.ERROR_LOGIN_FAILED, + status: FluidState.LOGIN_FAILED, maintenance: false, firstDataDate: null, lastDataDate: null, -- GitLab