-
Bastien DUMONT authoredBastien DUMONT authored
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