Skip to content
Snippets Groups Projects
SplashRoot.tsx 14 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'
    
      UsageEventType,
      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,
    
      UserChallenge,
    } 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'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import UsageEventService from 'services/usageEvent.service'
    
    import { setAnalysisMonth } from 'store/analysis/analysis.slice'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import {
      setChallengeConsumption,
      setUserChallengeList,
      updateUserChallengeList,
    
    } from 'store/challenge/challenge.slice'
    
    import { setSelectedDate } from 'store/chart/chart.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(
        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
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          const customPopupService = new CustomPopupService(client)
    
          const partnersInfoService = new PartnersInfoService(client)
    
          const ms = new MigrationService(client, setInitStepErrors)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          const startTime = performance.now()
          const transaction = Sentry.startTransaction({ name: 'Initialize app' })
    
            const termsStatus = await initializationService.initConsent()
    
            if (subscribed) dispatch(updateTermsStatus(termsStatus))
    
    Guilhem CARRON's avatar
     
    Guilhem CARRON committed
    
    
            // 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))
    
                await loadProfileType(profileType)
    
              if (profileEcogesture) {
    
                dispatch(setProfileEcogesture(profileEcogesture))
    
    Hugo's avatar
    Hugo committed
              dispatch(toggleAnalysisNotification(!profile.haveSeenLastAnalysis))
    
            // Init Fluid status && lastDate for the chart
    
            const fluidStatus = await initializationService.initFluidStatus()
    
            if (subscribed) {
              dispatch(setFluidStatus(fluidStatus))
    
              const refDate = DateTime.fromISO('0001-01-01')
    
              let lastDataDate: DateTime | null = DateTime.fromISO('0001-01-01')
              for (const fluid of fluidStatus) {
                if (fluid.lastDataDate && fluid.lastDataDate > lastDataDate) {
                  lastDataDate = fluid.lastDataDate
                }
              }
              if (lastDataDate > refDate) {
                dispatch(setSelectedDate(lastDataDate))
              }
    
            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
    
    Hugo's avatar
    Hugo committed
                dispatch(toggleChallengeExplorationNotification(true))
    
              // Set action to notification if action is accomplished
    
                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))
              }
    
    Yoan VALLET's avatar
    Yoan VALLET committed
              const filteredCurrentDuelChallenge = userChallengeList.filter(
                challenge => challenge.state === UserChallengeState.DUEL
    
                filteredCurrentDuelChallenge[0]?.duel.state ===
                UserDuelState.ONGOING
    
                const { updatedUserChallenge, dataloads } =
                  await initializationService.initDuelProgress(
                    filteredCurrentDuelChallenge[0]
                  )
    
                  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
                  )
    
    Hugo's avatar
    Hugo committed
                  dispatch(toggleChallengeDuelNotification(isDone))
    
            if (profile) {
              await UsageEventService.addEvent(client, {
    
                type: UsageEventType.CONNECTION_EVENT,
    
                result: profile.isFirstConnection ? 'firstConnection' : undefined,
    
                context:
                  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
                    navigator.userAgent
                  )
                    ? 'mobile'
                    : 'desktop',
    
            /**
             * 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)
              }
            })
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
              logDuration('[Initialization] Finished successfully !', startTime)
    
              setState(prev => ({
                ...prev,
                splashStart: true,
              }))
            }
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          } catch (error: any) {
            if (error.message === 'Failed to fetch' && !initStepErrors) {
    
              setInitStepErrors(InitStepsErrors.UNKNOWN_ERROR)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            logApp.error(`[Initialization] Error : ${error}`)
    
            Sentry.captureException(error)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          } 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