diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 4957dc6aa..44fad8584 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,7 +1,6 @@ import React, { ReactFragment, useState } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import type { UseMutateFunction } from 'react-query'; import styles from './Header.module.scss'; @@ -53,11 +52,11 @@ type Props = { accessModel?: AccessModel; profilesData?: { - currentProfile?: Profile; - profiles?: Profile[]; - profilesEnabled?: boolean; - selectProfile?: UseMutateFunction; - isSelectingProfile?: boolean; + currentProfile: Profile | null; + profiles: Profile[]; + profilesEnabled: boolean; + selectProfile: ({ avatarUrl, id }: { avatarUrl: string; id: string }) => void; + isSelectingProfile: boolean; }; }; @@ -138,7 +137,7 @@ const Header: React.FC = ({ {profilesEnabled && currentProfile ? ( - + ) : ( )} diff --git a/src/components/UserMenu/ProfilesMenu/ProfilesMenu.tsx b/src/components/UserMenu/ProfilesMenu/ProfilesMenu.tsx index fbc3635d7..ab9064e69 100644 --- a/src/components/UserMenu/ProfilesMenu/ProfilesMenu.tsx +++ b/src/components/UserMenu/ProfilesMenu/ProfilesMenu.tsx @@ -11,7 +11,7 @@ import type { Profile } from '#types/account'; type ProfilesMenuProps = { profiles: Profile[]; - currentProfile?: Profile; + currentProfile?: Profile | null; small?: boolean; selectingProfile: boolean; selectProfile: UseMutateFunction; diff --git a/src/components/UserMenu/UserMenu.tsx b/src/components/UserMenu/UserMenu.tsx index e0faff683..65777a9e6 100644 --- a/src/components/UserMenu/UserMenu.tsx +++ b/src/components/UserMenu/UserMenu.tsx @@ -2,7 +2,6 @@ import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import type { UseMutateFunction } from 'react-query'; import styles from './UserMenu.module.scss'; import ProfilesMenu from './ProfilesMenu/ProfilesMenu'; @@ -22,11 +21,11 @@ type Props = { showPaymentsItem: boolean; onClick?: () => void; accessModel?: AccessModel; - currentProfile?: Profile; + currentProfile?: Profile | null; profilesEnabled?: boolean; profiles?: Profile[]; isSelectingProfile?: boolean; - selectProfile?: UseMutateFunction; + selectProfile?: ({ id, avatarUrl }: { id: string; avatarUrl: string }) => void; }; const UserMenu = ({ diff --git a/src/containers/Layout/Layout.tsx b/src/containers/Layout/Layout.tsx index 47e598dee..41239089c 100644 --- a/src/containers/Layout/Layout.tsx +++ b/src/containers/Layout/Layout.tsx @@ -38,7 +38,10 @@ const Layout = () => { const supportedLanguages = useMemo(() => getSupportedLanguages(), []); const currentLanguage = useMemo(() => supportedLanguages.find(({ code }) => code === i18n.language), [i18n.language, supportedLanguages]); - const { data: { responseData: { collection: profiles = [] } = {} } = {}, profilesEnabled } = useProfiles(); + const { + query: { data: { responseData: { collection: profiles = [] } = {} } = {} }, + profilesEnabled, + } = useProfiles(); if (profilesEnabled && !profiles?.length) { unpersistProfile(); @@ -156,10 +159,10 @@ const Layout = () => { showPaymentsMenuItem={accessModel !== 'AVOD'} accessModel={accessModel} profilesData={{ - currentProfile: profile ?? undefined, + currentProfile: profile, profiles, profilesEnabled, - selectProfile: selectProfile.mutate, + selectProfile: ({ avatarUrl, id }) => selectProfile.mutate({ id, avatarUrl }), isSelectingProfile: !!selectProfile.isLoading, }} > diff --git a/src/containers/Profiles/CreateProfile.tsx b/src/containers/Profiles/CreateProfile.tsx index 8193c19c4..75ddc5595 100644 --- a/src/containers/Profiles/CreateProfile.tsx +++ b/src/containers/Profiles/CreateProfile.tsx @@ -13,7 +13,10 @@ import styles from '#src/pages/User/User.module.scss'; const CreateProfile = () => { const navigate = useNavigate(); - const { profilesEnabled } = useProfiles(); + const { + query: { isLoading: loadingProfilesList }, + profilesEnabled, + } = useProfiles(); const [avatarUrl, setAvatarUrl] = useState(AVATARS[Math.floor(Math.random() * AVATARS.length)]); @@ -24,8 +27,6 @@ const CreateProfile = () => { if (!profilesEnabled) navigate('/'); }, [profilesEnabled, navigate]); - const { isLoading: loadingProfilesList } = useProfiles(); - const initialValues = { name: '', adult: 'true', diff --git a/src/containers/Profiles/Profiles.tsx b/src/containers/Profiles/Profiles.tsx index 133728fc4..8f9b99018 100644 --- a/src/containers/Profiles/Profiles.tsx +++ b/src/containers/Profiles/Profiles.tsx @@ -33,7 +33,10 @@ const Profiles = ({ editMode = false }: Props) => { const breakpoint: Breakpoint = useBreakpoint(); const isMobile = breakpoint === Breakpoint.xs; - const { data, isLoading, isFetching, isError, profilesEnabled } = useProfiles(); + const { + query: { data, isLoading, isFetching, isError }, + profilesEnabled, + } = useProfiles(); const activeProfiles = data?.responseData.collection.length || 0; const canAddNew = activeProfiles < MAX_PROFILES; diff --git a/src/hooks/useProfiles.ts b/src/hooks/useProfiles.ts index d491a2dde..1ff6c60c0 100644 --- a/src/hooks/useProfiles.ts +++ b/src/hooks/useProfiles.ts @@ -8,7 +8,8 @@ import { useProfileStore } from '#src/stores/ProfileStore'; import type { CommonAccountResponse, ListProfilesResponse, ProfileDetailsPayload, ProfilePayload } from '#types/account'; import { useAccountStore } from '#src/stores/AccountStore'; import type { GenericFormErrors } from '#types/form'; -import type { ProfileFormSubmitError } from '#src/containers/Profiles/types'; +import type { ProfileFormSubmitError, ProfileFormValues } from '#src/containers/Profiles/types'; +import { logDev } from '#src/utils/common'; export const useSelectProfile = () => { const navigate = useNavigate(); @@ -24,13 +25,13 @@ export const useSelectProfile = () => { onError: () => { useProfileStore.setState({ selectingProfileAvatar: null }); navigate('/u/profiles'); - console.error('Unable to enter profile.'); + logDev('Unable to enter profile'); }, }); }; export const useCreateProfile = (options?: UseMutationOptions | undefined, unknown, ProfilePayload, unknown>) => { - const listProfiles = useProfiles(); + const { query: listProfiles } = useProfiles(); const navigate = useNavigate(); return useMutation | undefined, unknown, ProfilePayload, unknown>(createProfile, { @@ -46,7 +47,7 @@ export const useCreateProfile = (options?: UseMutationOptions | undefined, unknown, ProfilePayload, unknown>) => { - const listProfiles = useProfiles(); + const { query: listProfiles } = useProfiles(); const navigate = useNavigate(); return useMutation(updateProfile, { @@ -61,7 +62,7 @@ export const useUpdateProfile = (options?: UseMutationOptions | undefined, unknown, ProfileDetailsPayload, unknown>) => { - const listProfiles = useProfiles(); + const { query: listProfiles } = useProfiles(); const navigate = useNavigate(); return useMutation | undefined, unknown, ProfileDetailsPayload, unknown>(deleteProfile, { @@ -73,21 +74,13 @@ export const useDeleteProfile = (options?: UseMutationOptions !!e && typeof e === 'object' && 'message' in e; + export const useProfileErrorHandler = () => { const { t } = useTranslation('user'); - return ( - e: unknown, - setErrors: ( - errors: Partial< - Omit & { - adult: string; - } & GenericFormErrors - >, - ) => void, - ) => { - const formError = e as ProfileFormSubmitError; - if (formError.message.includes('409')) { + return (e: unknown, setErrors: (errors: Partial) => void) => { + if (isProfileFormSubmitError(e) && e.message.includes('409')) { setErrors({ name: t('profile.validation.name.already_exists') }); return; } @@ -99,11 +92,11 @@ export const useProfiles = ( options?: UseQueryOptions | undefined, unknown, ServiceResponse | undefined, string[]>, ) => { const user = useAccountStore((s) => s.user); - const query = useQuery(['listProfiles'], listProfiles, { ...options, enabled: !!user }); const { canManageProfiles } = useAccountStore(); + const query = useQuery(['listProfiles'], listProfiles, { ...options, enabled: !!user }); return { - ...query, - profilesEnabled: query.data?.responseData.canManageProfiles && canManageProfiles, + query, + profilesEnabled: !!(query.data?.responseData.canManageProfiles && canManageProfiles), }; }; diff --git a/src/stores/AccountController.ts b/src/stores/AccountController.ts index 615272276..ffdb7d70f 100644 --- a/src/stores/AccountController.ts +++ b/src/stores/AccountController.ts @@ -1,10 +1,9 @@ import i18next from 'i18next'; import { useProfileStore } from './ProfileStore'; -import { unpersistProfile } from './ProfileController'; +import { loadPersistedProfile, unpersistProfile } from './ProfileController'; import type { - Profile, Capture, Customer, CustomerConsent, @@ -29,7 +28,29 @@ import type { Offer } from '#types/checkout'; const PERSIST_PROFILE = 'profile'; -export const initializeAccount = async ({ profile }: { profile?: Profile } = {}) => { +export const loadUserData = async () => { + await useService(async ({ accountService }) => { + try { + const authData = await accountService.getAuthData(); + + if (authData) { + await getAccount(); + await restoreWatchHistory(); + await restoreFavorites(); + } + } catch (error: unknown) { + logDev('Failed to get user', error); + + // clear the session when the token was invalid + // don't clear the session when the error is unknown (network hiccup or something similar) + if (error instanceof Error && error.message.includes('Invalid JWT token')) { + await logout(); + } + } + }); +}; + +export const initializeAccount = async () => { await useService(async ({ accountService, config }) => { if (!accountService) { useAccountStore.setState({ loading: false }); @@ -48,47 +69,16 @@ export const initializeAccount = async ({ profile }: { profile?: Profile } = {}) canShowReceipts: accountService.canShowReceipts, }); - useProfileStore.getState().setProfile(persist.getItem(PERSIST_PROFILE) || null); + loadPersistedProfile(); await accountService.initialize(config, logout); - try { - if (profile?.credentials?.access_token) { - loadProfile(profile); - } - const authData = await accountService.getAuthData(); - - if (authData) { - await getAccount(); - await restoreWatchHistory(); - await restoreFavorites(); - } - } catch (error: unknown) { - logDev('Failed to get user', error); - - // clear the session when the token was invalid - // don't clear the session when the error is unknown (network hiccup or something similar) - if (error instanceof Error && error.message.includes('Invalid JWT token')) { - await logout(); - } - } + await loadUserData(); useAccountStore.setState({ loading: false }); }); }; -const loadProfile = (profile: Profile) => { - persist.setItem(PERSIST_PROFILE, profile); - persist.setItemStorage('inplayer_token', { - expires: profile.credentials.expires, - token: profile.credentials.access_token, - refreshToken: '', - }); - useFavoritesStore.setState({ favorites: [] }); - useWatchHistoryStore.setState({ watchHistory: [] }); - useProfileStore.getState().setProfile(profile); -}; - export async function updateUser(values: FirstLastNameInput | EmailConfirmPasswordInput): Promise> { return await useService(async ({ accountService, sandbox = true }) => { useAccountStore.setState({ loading: true }); diff --git a/src/stores/ProfileController.ts b/src/stores/ProfileController.ts index fe914a33c..c44735204 100644 --- a/src/stores/ProfileController.ts +++ b/src/stores/ProfileController.ts @@ -1,5 +1,10 @@ -import { initializeAccount } from './AccountController'; +import type { ProfilesData } from '@inplayer-org/inplayer.js'; + import { useAccountStore } from './AccountStore'; +import { useFavoritesStore } from './FavoritesStore'; +import { useProfileStore } from './ProfileStore'; +import { useWatchHistoryStore } from './WatchHistoryStore'; +import { loadUserData } from './AccountController'; import * as persist from '#src/utils/persist'; import type { ProfilePayload, EnterProfilePayload, ProfileDetailsPayload } from '#types/account'; @@ -11,6 +16,48 @@ export const unpersistProfile = () => { persist.removeItem(PERSIST_PROFILE); }; +export const persistProfile = ({ profile }: { profile: ProfilesData }) => { + persist.setItem(PERSIST_PROFILE, profile); + persist.setItemStorage('inplayer_token', { + expires: profile.credentials.expires, + token: profile.credentials.access_token, + refreshToken: '', + }); +}; + +export const isValidProfile = (profile: unknown): profile is ProfilesData => { + return ( + typeof profile === 'object' && + profile !== null && + 'id' in profile && + 'name' in profile && + 'avatar_url' in profile && + 'adult' in profile && + 'credentials' in profile + ); +}; + +export const loadPersistedProfile = () => { + const profile = persist.getItem(PERSIST_PROFILE); + if (isValidProfile(profile)) { + useProfileStore.getState().setProfile(profile); + return profile; + } + useProfileStore.getState().setProfile(null); + return null; +}; + +export const initializeProfile = async ({ profile }: { profile: ProfilesData }) => { + persistProfile({ profile }); + useFavoritesStore.setState({ favorites: [] }); + useWatchHistoryStore.setState({ watchHistory: [] }); + useProfileStore.getState().setProfile(profile); + + await loadUserData(); + + return profile; +}; + export const listProfiles = async () => { return await useService(async ({ profileService, sandbox }) => { const res = await profileService?.listProfiles(undefined, sandbox ?? true); @@ -38,9 +85,10 @@ export const enterProfile = async ({ id, pin }: EnterProfilePayload) => { return await useService(async ({ profileService, sandbox }) => { const response = await profileService?.enterProfile({ id, pin }, sandbox ?? true); const profile = response?.responseData; - return initializeAccount({ - profile, - }); + if (!profile) { + throw new Error('Unable to enter profile'); + } + await initializeProfile({ profile }); }); };