Skip to content

Commit

Permalink
fix: refactor profile and account controllers to avoid calling initia…
Browse files Browse the repository at this point in the history
…lizeAccount multiple times
  • Loading branch information
naumovski-filip committed Oct 18, 2023
1 parent 519f76e commit 1f08006
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 78 deletions.
13 changes: 6 additions & 7 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -53,11 +52,11 @@ type Props = {

accessModel?: AccessModel;
profilesData?: {
currentProfile?: Profile;
profiles?: Profile[];
profilesEnabled?: boolean;
selectProfile?: UseMutateFunction<unknown, unknown, { id: string; avatarUrl: string }, unknown>;
isSelectingProfile?: boolean;
currentProfile: Profile | null;
profiles: Profile[];
profilesEnabled: boolean;
selectProfile: ({ avatarUrl, id }: { avatarUrl: string; id: string }) => void;
isSelectingProfile: boolean;
};
};

Expand Down Expand Up @@ -138,7 +137,7 @@ const Header: React.FC<Props> = ({
<React.Fragment>
<IconButton className={classNames(styles.iconButton, styles.actionButton)} aria-label={t('open_user_menu')} onClick={openUserMenu}>
{profilesEnabled && currentProfile ? (
<ProfileCircle src={currentProfile.avatar_url} alt={currentProfile?.name ?? t('profile_icon')} />
<ProfileCircle src={currentProfile.avatar_url} alt={currentProfile.name || t('profile_icon')} />
) : (
<AccountCircle />
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/UserMenu/ProfilesMenu/ProfilesMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown, unknown, { id: string; avatarUrl: string }, unknown>;
Expand Down
5 changes: 2 additions & 3 deletions src/components/UserMenu/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<unknown, unknown, { id: string; avatarUrl: string }, unknown>;
selectProfile?: ({ id, avatarUrl }: { id: string; avatarUrl: string }) => void;
};

const UserMenu = ({
Expand Down
9 changes: 6 additions & 3 deletions src/containers/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
}}
>
Expand Down
7 changes: 4 additions & 3 deletions src/containers/Profiles/CreateProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(AVATARS[Math.floor(Math.random() * AVATARS.length)]);

Expand All @@ -24,8 +27,6 @@ const CreateProfile = () => {
if (!profilesEnabled) navigate('/');
}, [profilesEnabled, navigate]);

const { isLoading: loadingProfilesList } = useProfiles();

const initialValues = {
name: '',
adult: 'true',
Expand Down
5 changes: 4 additions & 1 deletion src/containers/Profiles/Profiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 13 additions & 20 deletions src/hooks/useProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<ServiceResponse<ProfilesData> | undefined, unknown, ProfilePayload, unknown>) => {
const listProfiles = useProfiles();
const { query: listProfiles } = useProfiles();
const navigate = useNavigate();

return useMutation<ServiceResponse<ProfilesData> | undefined, unknown, ProfilePayload, unknown>(createProfile, {
Expand All @@ -46,7 +47,7 @@ export const useCreateProfile = (options?: UseMutationOptions<ServiceResponse<Pr
};

export const useUpdateProfile = (options?: UseMutationOptions<ServiceResponse<ProfilesData> | undefined, unknown, ProfilePayload, unknown>) => {
const listProfiles = useProfiles();
const { query: listProfiles } = useProfiles();
const navigate = useNavigate();

return useMutation(updateProfile, {
Expand All @@ -61,7 +62,7 @@ export const useUpdateProfile = (options?: UseMutationOptions<ServiceResponse<Pr
};

export const useDeleteProfile = (options?: UseMutationOptions<ServiceResponse<CommonAccountResponse> | undefined, unknown, ProfileDetailsPayload, unknown>) => {
const listProfiles = useProfiles();
const { query: listProfiles } = useProfiles();
const navigate = useNavigate();

return useMutation<ServiceResponse<CommonAccountResponse> | undefined, unknown, ProfileDetailsPayload, unknown>(deleteProfile, {
Expand All @@ -73,21 +74,13 @@ export const useDeleteProfile = (options?: UseMutationOptions<ServiceResponse<Co
});
};

export const isProfileFormSubmitError = (e: unknown): e is ProfileFormSubmitError => !!e && typeof e === 'object' && 'message' in e;

export const useProfileErrorHandler = () => {
const { t } = useTranslation('user');

return (
e: unknown,
setErrors: (
errors: Partial<
Omit<ProfilePayload, 'adult'> & {
adult: string;
} & GenericFormErrors
>,
) => void,
) => {
const formError = e as ProfileFormSubmitError;
if (formError.message.includes('409')) {
return (e: unknown, setErrors: (errors: Partial<ProfileFormValues & GenericFormErrors>) => void) => {
if (isProfileFormSubmitError(e) && e.message.includes('409')) {
setErrors({ name: t('profile.validation.name.already_exists') });
return;
}
Expand All @@ -99,11 +92,11 @@ export const useProfiles = (
options?: UseQueryOptions<ServiceResponse<ListProfilesResponse> | undefined, unknown, ServiceResponse<ListProfilesResponse> | 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),
};
};
62 changes: 26 additions & 36 deletions src/stores/AccountController.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 });
Expand All @@ -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<ServiceResponse<Customer>> {
return await useService(async ({ accountService, sandbox = true }) => {
useAccountStore.setState({ loading: true });
Expand Down
56 changes: 52 additions & 4 deletions src/stores/ProfileController.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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 });
});
};

Expand Down

0 comments on commit 1f08006

Please sign in to comment.