import * as Sentry from '@sentry/react' import classNames from 'classnames' import useExploration from 'components/Hooks/useExploration' import { useClient } from 'cozy-client' import { UserActionState, UserChallengeState, UserDuelState, UserExplorationID, UserExplorationState, } from 'enums' import { DateTime } from 'luxon' import { migrations } from 'migrations/migration.data' import { MigrationService } from 'migrations/migration.service' import { CustomPopup, InitSteps, InitStepsErrors, PartnersInfo, Profile, ProfileType, UserChallenge, } from 'models' import React, { ReactNode, useCallback, useEffect, useState } from 'react' import ActionService from 'services/action.service' import ChallengeService from 'services/challenge.service' import CustomPopupService from 'services/customPopup.service' import FluidService from 'services/fluid.service' import InitializationService from 'services/initialization.service' import PartnersInfoService from 'services/partnersInfo.service' import ProfileTypeEntityService from 'services/profileTypeEntity.service' import { setAnalysisMonth } from 'store/analysis/analysis.slice' import { setChallengeConsumption, setUserChallengeList, updateUserChallengeList, } from 'store/challenge/challenge.slice' import { setFluidStatus, showReleaseNotes, toggleAnalysisNotification, toggleChallengeActionNotification, toggleChallengeDuelNotification, toggleChallengeExplorationNotification, updateTermsStatus, } from 'store/global/global.slice' import { useAppDispatch } from 'store/hooks' import { openPartnersModal, setCustomPopup } from 'store/modal/modal.slice' import { updateProfile } from 'store/profile/profile.slice' import { setProfileEcogesture } from 'store/profileEcogesture/profileEcogesture.slice' import { setProfileType } from 'store/profileType/profileType.slice' import { logDuration } from 'utils/duration' import logApp from 'utils/logger' import { getTodayDate } from 'utils/utils' import SplashScreen from './SplashScreen' import SplashScreenError from './SplashScreenError' import './splashRoot.scss' interface SplashRootProps { fadeTimer?: number children: ReactNode } /** * Added splash screen if data is not ready * @param params {{ fadeTimer, splashComponent, children }} */ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { const client = useClient() const today = getTodayDate().toISO() const dispatch = useAppDispatch() const [{ splashEnd, splashStart }, setState] = useState({ splashEnd: false, splashStart: false, }) const [initStep, setInitStep] = useState<InitSteps>(InitSteps.MIGRATION) const [initStepErrors, setInitStepErrors] = useState<InitStepsErrors | null>( null ) /** Return current status of partner if modal has not been seen today */ const getPartnerStatus = useCallback( (currentStatus: boolean, lastSeenDate: DateTime) => { if (today !== lastSeenDate.toISO()) { return currentStatus } return false }, [today] ) /** For each fluid, if notification is activated, set FluidStatus.maintenance to true */ const processFluidsStatus = useCallback( async (profile: Profile, partnersInfo: PartnersInfo) => { if (partnersInfo.notification_activated && !profile?.isFirstConnection) { const fluidService = new FluidService(client) const _updatedFluidStatus = await fluidService.getFluidStatus(partnersInfo) dispatch(setFluidStatus(_updatedFluidStatus)) } }, [client, dispatch] ) /** Process customPopup and enable it if activated */ const processCustomPopup = useCallback( async (profile: Profile, customPopup: CustomPopup) => { try { if ( today !== profile?.customPopupDate.toISO() && !profile?.isFirstConnection ) { dispatch(setCustomPopup(customPopup)) } } catch (error) { console.error('Error while checking customPopup informations') } }, [dispatch, today] ) /** * For each fluid, set partnersIssue to true if notification is activated and seenDate < today */ const processPartnersStatus = useCallback( async (profile: Profile, partnersInfo: PartnersInfo) => { try { if ( partnersInfo.notification_activated && !profile?.isFirstConnection ) { const partnersIssue = { enedis: getPartnerStatus( partnersInfo.enedis_failure, profile.partnersIssueSeenDate.enedis ), egl: getPartnerStatus( partnersInfo.egl_failure, profile.partnersIssueSeenDate.egl ), grdf: getPartnerStatus( partnersInfo.grdf_failure, profile.partnersIssueSeenDate.grdf ), } if (Object.values(partnersIssue).some(issue => issue)) { dispatch(openPartnersModal(partnersIssue)) } } } catch (error) { console.error('Error while fetching partners informations') } }, [dispatch, getPartnerStatus] ) const loadProfileType = useCallback( async (profileType: ProfileType) => { const profileTypeEntityService = new ProfileTypeEntityService(client) const updatedProfileType = await profileTypeEntityService.saveProfileType(profileType) if (updatedProfileType) { dispatch(setProfileType(updatedProfileType)) } }, [client, dispatch] ) useEffect(() => { let timeoutSplash: NodeJS.Timeout if (splashStart) { timeoutSplash = setTimeout(() => { setState(prev => ({ ...prev, splashEnd: true })) }, fadeTimer) } return () => timeoutSplash && clearTimeout(timeoutSplash) }, [splashStart, fadeTimer]) const [, setValidExploration] = useExploration() useEffect(() => { let subscribed = true async function loadData() { const initializationService = new InitializationService( client, setInitStep, setInitStepErrors ) const customPopupService = new CustomPopupService(client) const partnersInfoService = new PartnersInfoService(client) const ms = new MigrationService(client, setInitStepErrors) const startTime = performance.now() const transaction = Sentry.startTransaction({ name: 'Initialize app' }) try { console.groupCollapsed('Initialization logs') // init Terms const termsStatus = await initializationService.initConsent() if (subscribed) dispatch(updateTermsStatus(termsStatus)) // Init fluidPrices await initializationService.initFluidPrices() // Init profile and update ecogestures, challenges, analysis const profile = await initializationService.initProfile() const profileType = await initializationService.initProfileType() const profileEcogesture = await initializationService.initProfileEcogesture() const migrationsResult = await ms.runMigrations(migrations) // Init last release notes when they exist dispatch( showReleaseNotes({ notes: migrationsResult.notes, redirectLink: migrationsResult.redirectLink, show: migrationsResult.show, }) ) if (subscribed && profile) { setValidExploration(UserExplorationID.EXPLORATION007) const [ duelHash, quizHash, challengeHash, explorationHash, analysisResult, ] = await Promise.all([ initializationService.initDuelEntity(profile.duelHash), initializationService.initQuizEntity(profile.quizHash), initializationService.initExplorationEntity(profile.challengeHash), initializationService.initChallengeEntity(profile.explorationHash), initializationService.initAnalysis(profile), ]) const updatedProfile: Partial<Profile> = { duelHash, quizHash, challengeHash, explorationHash, monthlyAnalysisDate: analysisResult.monthlyAnalysisDate, haveSeenLastAnalysis: analysisResult.haveSeenLastAnalysis, } dispatch(updateProfile(updatedProfile)) dispatch(setAnalysisMonth(analysisResult.monthlyAnalysisDate)) if (profileType) { await loadProfileType(profileType) } if (profileEcogesture) { dispatch(setProfileEcogesture(profileEcogesture)) } dispatch(toggleAnalysisNotification(!profile.haveSeenLastAnalysis)) } // Init Fluid status && lastDate for the chart const fluidStatus = await initializationService.initFluidStatus() if (subscribed) { dispatch(setFluidStatus(fluidStatus)) let lastDataDate: DateTime | null = DateTime.fromISO('0001-01-01') for (const fluid of fluidStatus) { if (fluid.lastDataDate && fluid.lastDataDate > lastDataDate) { lastDataDate = fluid.lastDataDate } } } // Init Challenge const userChallengeList = await initializationService.initUserChallenges(fluidStatus) if (subscribed) { dispatch(setUserChallengeList(userChallengeList)) const filteredCurrentOngoingChallenge = userChallengeList.filter( challenge => challenge.state === UserChallengeState.ONGOING ) // Set Notification if exploration state is notification if ( filteredCurrentOngoingChallenge[0]?.exploration.state === UserExplorationState.NOTIFICATION ) { dispatch(toggleChallengeExplorationNotification(true)) } // Set action to notification if action is accomplished if ( filteredCurrentOngoingChallenge[0]?.action.state === UserActionState.ONGOING ) { const actionService = new ActionService(client) const updatedUserChallenge: UserChallenge | null = await actionService.isActionDone( filteredCurrentOngoingChallenge[0] ) if (updatedUserChallenge) { dispatch(updateUserChallengeList(updatedUserChallenge)) } } // Set Notification if action state is notification if ( filteredCurrentOngoingChallenge[0]?.action.state === UserActionState.NOTIFICATION ) { dispatch(toggleChallengeActionNotification(true)) } const filteredCurrentDuelChallenge = userChallengeList.filter( challenge => challenge.state === UserChallengeState.DUEL ) if ( filteredCurrentDuelChallenge[0]?.duel.state === UserDuelState.ONGOING ) { const { updatedUserChallenge, dataloads } = await initializationService.initDuelProgress( filteredCurrentDuelChallenge[0] ) if (subscribed) { dispatch( setChallengeConsumption({ userChallenge: updatedUserChallenge, currentDataload: dataloads, }) ) // Check is duel is done and display notification const challengeService = new ChallengeService(client) const { isDone } = await challengeService.isChallengeDone( updatedUserChallenge, dataloads ) dispatch(toggleChallengeDuelNotification(isDone)) } } } /** * Load custom popup and partners info synchronously so these treatments don't block the loading */ customPopupService.getCustomPopup().then(async customPopup => { if (profile && customPopup) { await processCustomPopup(profile, customPopup) } }) partnersInfoService.getPartnersInfo().then(async partnersInfo => { if (profile && partnersInfo) { await processFluidsStatus(profile, partnersInfo) await processPartnersStatus(profile, partnersInfo) } }) if (subscribed) { logDuration('[Initialization] Finished successfully !', startTime) setState(prev => ({ ...prev, splashStart: true, })) } } catch (error: any) { if (error.message === 'Failed to fetch' && !initStepErrors) { setInitStepErrors(InitStepsErrors.UNKNOWN_ERROR) } logApp.error(`[Initialization] Error : ${error}`) Sentry.captureException(error) } finally { console.groupEnd() transaction.finish() } } if (!initStepErrors) loadData() return () => { subscribed = false } }, [ client, dispatch, initStepErrors, loadProfileType, processCustomPopup, processFluidsStatus, processPartnersStatus, setValidExploration, ]) return ( <> {!splashEnd && ( <div style={{ transitionDuration: `${fadeTimer / 1000}s` }} className={classNames('splash-root', { ['splash-fade']: splashStart, })} > {!initStepErrors ? ( <SplashScreen initStep={initStep} /> ) : ( <SplashScreenError error={initStepErrors} /> )} </div> )} {splashStart && children} </> ) } export default SplashRoot