Skip to content
Snippets Groups Projects
SplashRoot.tsx 13.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • import * as Sentry from '@sentry/react'
    
    import classNames from 'classnames'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import useExploration from 'components/Hooks/useExploration'
    
    import { useClient } from 'cozy-client'
    
      UserActionState,
      UserChallengeState,
      UserDuelState,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      UserExplorationID,
      UserExplorationState,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import { DateTime } from 'luxon'
    import { migrations } from 'migrations/migration.data'
    import { MigrationService } from 'migrations/migration.service'
    
    import {
      CustomPopup,
      InitSteps,
      InitStepsErrors,
      PartnersInfo,
      Profile,
    
      ProfileType,
    
    } from 'models'
    
    import React, { ReactNode, useCallback, useEffect, useState } from 'react'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import ActionService from 'services/action.service'
    import ChallengeService from 'services/challenge.service'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import CustomPopupService from 'services/customPopup.service'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    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'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import {
      setChallengeConsumption,
      setUserChallengeList,
      updateUserChallengeList,
    
    } from 'store/challenge/challenge.slice'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import {
      setFluidStatus,
      showReleaseNotes,
    
    Hugo's avatar
    Hugo committed
      toggleAnalysisNotification,
      toggleChallengeActionNotification,
      toggleChallengeDuelNotification,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      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'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import { logDuration } from 'utils/duration'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    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>(
    
      /** 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(
    
        (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(
    
        (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
    
        function loadData() {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          const startTime = performance.now()
    
          Sentry.startSpan({ name: 'Initialize app' }, async () => {
            const initializationService = new InitializationService(
              client,
              setInitStepErrors
    
            const customPopupService = new CustomPopupService(client)
            const partnersInfoService = new PartnersInfoService(client)
            const ms = new MigrationService(client, setInitStepErrors)
            try {
              // Run init steps in parallel
              setInitStep(InitSteps.PROFILE)
    
                termsStatus,
                ,
                profile,
                profileType,
                profileEcogesture,
                fluidStatus,
    
              ] = await Promise.all([
    
                initializationService.initConsent(),
                initializationService.initFluidPrices(),
                initializationService.initProfile(),
                initializationService.initProfileType(),
                initializationService.initProfileEcogesture(),
                initializationService.initFluidStatus(),
    
              if (subscribed) dispatch(updateTermsStatus(termsStatus))
    
              setInitStep(InitSteps.MIGRATION)
              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)
                setInitStep(InitSteps.CHALLENGES)
                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))
    
    
              // Process fluids status
              if (subscribed) {
                dispatch(setFluidStatus(fluidStatus))
                let lastDataDate = 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 = 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 } = 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(customPopup => {
    
                if (profile && customPopup) {
    
                  processCustomPopup(profile, customPopup)
    
                }
              })
              partnersInfoService.getPartnersInfo().then(async partnersInfo => {
                if (profile && partnersInfo) {
                  await processFluidsStatus(profile, partnersInfo)
    
                  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)
    
        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