Skip to content
Snippets Groups Projects
SplashRoot.tsx 13.18 KiB
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,
} 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 {
        // Load app in parallel
        const [
          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))

        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))
        }

        // 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 } = 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 {
        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