diff --git a/packages/extension/src/companion/Companion.tsx b/packages/extension/src/companion/Companion.tsx index 14898502a2..7ef39a6a0c 100644 --- a/packages/extension/src/companion/Companion.tsx +++ b/packages/extension/src/companion/Companion.tsx @@ -110,13 +110,11 @@ export default function Companion({ }), }); const [assetsLoadedDebounce] = useDebounceFn(() => setAssetsLoaded(true), 10); - const routeChangedCallbackRef = useLogPageView(); + const routeChangedCallback = useLogPageView(); useEffect(() => { - if (routeChangedCallbackRef.current) { - routeChangedCallbackRef.current(); - } - }, [routeChangedCallbackRef]); + routeChangedCallback?.(); + }, [routeChangedCallback]); const [checkAssets, clearCheckAssets] = useDebounceFn(() => { if (containerRef?.current?.offsetLeft === 0) { @@ -133,7 +131,7 @@ export default function Companion({ } checkAssets(); - routeChangedCallbackRef.current(); + routeChangedCallback?.(); // @NOTE see https://dailydotdev.atlassian.net/l/cp/dK9h1zoM // eslint-disable-next-line react-hooks/exhaustive-deps }, [containerRef]); diff --git a/packages/extension/src/newtab/App.tsx b/packages/extension/src/newtab/App.tsx index fd46eb39ed..2d7437c3bc 100644 --- a/packages/extension/src/newtab/App.tsx +++ b/packages/extension/src/newtab/App.tsx @@ -108,7 +108,7 @@ function InternalApp(): ReactElement { const { contentScriptGranted } = useContentScriptStatus(); const { hostGranted, isFetching: isCheckingHostPermissions } = useHostStatus(); - const routeChangedCallbackRef = useLogPageView(); + const routeChangedCallback = useLogPageView(); useConsoleLogo(); const { value: extensionOverlay } = useConditionalFeature({ @@ -122,10 +122,10 @@ function InternalApp(): ReactElement { const shouldRedirectOnboarding = !user && isPageReady && !isTesting; useEffect(() => { - if (routeChangedCallbackRef.current && isPageReady) { - routeChangedCallbackRef.current(); + if (isPageReady && currentPage) { + routeChangedCallback?.(); } - }, [isPageReady, routeChangedCallbackRef, currentPage]); + }, [isPageReady, routeChangedCallback, currentPage]); const { dismissToast } = useToastNotification(); diff --git a/packages/shared/src/contexts/AuthContext.tsx b/packages/shared/src/contexts/AuthContext.tsx index 44cf1a356f..641badf673 100644 --- a/packages/shared/src/contexts/AuthContext.tsx +++ b/packages/shared/src/contexts/AuthContext.tsx @@ -17,7 +17,7 @@ import { AccessToken, Boot, Visit } from '../lib/boot'; import { isCompanionActivated } from '../lib/element'; import { AuthTriggers, AuthTriggersType } from '../lib/auth'; import { Squad } from '../graphql/sources'; -import { checkIsExtension, isNullOrUndefined } from '../lib/func'; +import { checkIsExtension } from '../lib/func'; export interface LoginState { trigger: AuthTriggersType; @@ -103,7 +103,7 @@ export type AuthContextProviderProps = { isFetched?: boolean; isLegacyLogout?: boolean; children?: ReactNode; - firstLoad?: boolean; + isAuthReady?: boolean; } & Pick< AuthContextData, | 'getRedirectUri' @@ -131,14 +131,14 @@ export const AuthContextProvider = ({ isLegacyLogout, accessToken, squads, - firstLoad, + isAuthReady, }: AuthContextProviderProps): ReactElement => { const [loginState, setLoginState] = useState(null); const endUser = user && 'providers' in user ? user : null; const referral = user?.referralId || user?.referrer; const referralOrigin = user?.referralOrigin; - if (firstLoad === true && endUser && !endUser?.infoConfirmed) { + if (isAuthReady === true && endUser && !endUser?.infoConfirmed) { logout(LogoutReason.IncomleteOnboarding); } @@ -149,7 +149,7 @@ export const AuthContextProvider = ({ return ( { +const getCachedBootOrNull = () => { try { return JSON.parse(storage.getItem(BOOT_LOCAL_KEY)); } catch (err) { @@ -111,6 +110,8 @@ export const BootDataProvider = ({ getRedirectUri, getPage, }: BootDataProviderProps): ReactElement => { + const { hostGranted } = useHostStatus(); + const isExtension = checkIsExtension(); const queryClient = useQueryClient(); const preloadFeedsRef = useRef(); preloadFeedsRef.current = ({ feeds, user }) => { @@ -129,8 +130,7 @@ export const BootDataProvider = ({ ); }; - const [initialLoad, setInitialLoad] = useState(null); - const [cachedBootData, setCachedBootData] = useState>(() => { + const initialData = useMemo(() => { if (localBootData) { return localBootData; } @@ -148,15 +148,14 @@ export const BootDataProvider = ({ preloadFeedsRef.current({ feeds: boot.feeds, user: boot.user }); return boot; - }); - const { hostGranted } = useHostStatus(); - const isExtension = checkIsExtension(); - const logged = cachedBootData?.user as LoggedUser; + }, [localBootData]); + + const logged = initialData?.user as LoggedUser; const shouldRefetch = !!logged?.providers && !!logged?.id; const lastAppliedChangeRef = useRef>(); const { - data: remoteData, + data: bootData, error, refetch, isFetched, @@ -167,24 +166,25 @@ export const BootDataProvider = ({ queryFn: async () => { const result = await getBootData(app); preloadFeedsRef.current({ feeds: result.feeds, user: result.user }); + updateLocalBootData(bootData || {}, result); return result; }, refetchOnWindowFocus: shouldRefetch, staleTime: STALE_TIME, enabled: !isExtension || !!hostGranted, + placeholderData: initialData, }); - const isBootReady = isFetched && !isError; - const loadedFromCache = !!cachedBootData; - const { user, settings, alerts, notifications, squads } = - cachedBootData || {}; + const isBootReady = isFetched && !isError && !!bootData; + const loadedFromCache = !!bootData; + const { user, settings, alerts, notifications, squads } = bootData || {}; - useRefreshToken(remoteData?.accessToken, refetch); + useRefreshToken(bootData?.accessToken, refetch); const updatedAtActive = user ? dataUpdatedAt : null; - const updateBootData = useCallback( + const updateQueryCache = useCallback( (updatedBootData: Partial, update = true) => { - const cachedData = getCachedOrNull() || {}; + const cachedData = getCachedBootOrNull() ?? {}; const lastAppliedChange = lastAppliedChangeRef.current; let updatedData = { ...updatedBootData }; if (update) { @@ -200,36 +200,45 @@ export const BootDataProvider = ({ } const updated = updateLocalBootData(cachedData, updatedData); - setCachedBootData(updated); + + queryClient.setQueryData>(BOOT_QUERY_KEY, (previous) => { + if (!previous) { + return updated; + } + + return { ...previous, ...updated }; + }); }, - [], + [queryClient], ); const updateUser = useCallback( async (newUser: LoggedUser | AnonymousUser) => { - updateBootData({ user: newUser }); + updateQueryCache({ user: newUser }); await queryClient.invalidateQueries({ queryKey: generateQueryKey(RequestKey.Profile, newUser), }); }, - [updateBootData, queryClient], + [updateQueryCache, queryClient], ); const updateSettings = useCallback( - (updatedSettings) => updateBootData({ settings: updatedSettings }), - [updateBootData], + (updatedSettings: Boot['settings']) => + updateQueryCache({ settings: updatedSettings }), + [updateQueryCache], ); const updateAlerts = useCallback( - (updatedAlerts) => updateBootData({ alerts: updatedAlerts }), - [updateBootData], + (updatedAlerts: Boot['alerts']) => + updateQueryCache({ alerts: updatedAlerts }), + [updateQueryCache], ); const updateExperimentation = useCallback( (exp: BootCacheData['exp']) => { - updateLocalBootData(cachedBootData, { exp }); + updateLocalBootData(bootData, { exp }); }, - [cachedBootData], + [bootData], ); gqlClient.setHeader( @@ -237,14 +246,6 @@ export const BootDataProvider = ({ (user as Partial)?.language || ContentLanguage.English, ); - useEffect(() => { - if (remoteData) { - setInitialLoad(initialLoad === null); - updateBootData(remoteData); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [remoteData]); - if (error) { return (
@@ -258,7 +259,7 @@ export const BootDataProvider = ({ app={app} user={user} deviceId={deviceId} - experimentation={cachedBootData?.exp} + experimentation={bootData?.exp} updateExperimentation={updateExperimentation} > >, sendBeacon: () => void, ): LogContextData { - return useMemo( - () => ({ - logEvent(event: LogEvent) { - pushToQueue([generateEvent(event, sharedPropsRef, getPage())]); - }, - logEventStart(id, event) { - if (!durationEventsQueue.current.has(id)) { - durationEventsQueue.current.set( - id, - generateEvent(event, sharedPropsRef, getPage()), - ); - } - }, - logEventEnd(id, now = new Date()) { - const event = durationEventsQueue.current.get(id); - if (event) { - durationEventsQueue.current.delete(id); - event.event_duration = - now.getTime() - event.event_timestamp.getTime(); - if (window.scrollY > 0 && event.event_name !== 'page inactive') { - event.page_state = 'active'; - } - pushToQueue([event]); + const logEvent = useCallback( + (event: LogEvent) => { + pushToQueue([generateEvent(event, sharedPropsRef, getPage())]); + }, + [getPage, pushToQueue, sharedPropsRef], + ); + const logEventStart = useCallback( + (id, event) => { + if (!durationEventsQueue.current.has(id)) { + durationEventsQueue.current.set( + id, + generateEvent(event, sharedPropsRef, getPage()), + ); + } + }, + [durationEventsQueue, getPage, sharedPropsRef], + ); + const logEventEnd = useCallback( + (id, now = new Date()) => { + const event = durationEventsQueue.current.get(id); + if (event) { + durationEventsQueue.current.delete(id); + event.event_duration = now.getTime() - event.event_timestamp.getTime(); + if (window.scrollY > 0 && event.event_name !== 'page inactive') { + event.page_state = 'active'; } - }, - sendBeacon, - }), - [sharedPropsRef, getPage, pushToQueue, durationEventsQueue, sendBeacon], + pushToQueue([event]); + } + }, + [durationEventsQueue, pushToQueue], ); + + return { + logEvent, + logEventStart, + logEventEnd, + sendBeacon, + }; } diff --git a/packages/shared/src/hooks/log/useLogPageView.ts b/packages/shared/src/hooks/log/useLogPageView.ts index 6f9868d471..f8e93ac92f 100644 --- a/packages/shared/src/hooks/log/useLogPageView.ts +++ b/packages/shared/src/hooks/log/useLogPageView.ts @@ -1,32 +1,29 @@ -import { MutableRefObject, useContext, useEffect, useRef } from 'react'; +import { useCallback, useContext, useEffect } from 'react'; import { useRouter } from 'next/router'; import LogContext from '../../contexts/LogContext'; -export default function useLogPageView(): MutableRefObject<() => void> { +export default function useLogPageView(): () => void { const router = useRouter(); const { logEventStart, logEventEnd } = useContext(LogContext); - const routeChangedCallbackRef = useRef<() => void>(); - const lifecycleCallbackRef = useRef<(event: CustomEvent) => void>(); + const routeChangedCallback = useCallback(() => { + logEventEnd('page view'); + logEventStart('page view', { event_name: 'page view' }); + }, [logEventEnd, logEventStart]); - useEffect(() => { - routeChangedCallbackRef.current = () => { - logEventEnd('page view'); - logEventStart('page view', { event_name: 'page view' }); - }; - - lifecycleCallbackRef.current = (event) => { + const lifecycleCallback = useCallback( + (event: CustomEvent) => { if (event.detail.newState === 'active') { logEventStart('page view', { event_name: 'page view' }); } - }; - }, [logEventStart, logEventEnd]); + }, + [logEventStart], + ); useEffect(() => { - const handleRouteChange = () => routeChangedCallbackRef.current(); + const handleRouteChange = () => routeChangedCallback(); router.events.on('routeChangeComplete', handleRouteChange); - const handleLifecycle = (event: CustomEvent) => - lifecycleCallbackRef.current(event); + const handleLifecycle = (event: CustomEvent) => lifecycleCallback(event); window.addEventListener('statechange', handleLifecycle); return () => { @@ -35,7 +32,7 @@ export default function useLogPageView(): MutableRefObject<() => void> { }; // @NOTE see https://dailydotdev.atlassian.net/l/cp/dK9h1zoM // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [lifecycleCallback, routeChangedCallback]); - return routeChangedCallbackRef; + return routeChangedCallback; } diff --git a/packages/shared/src/hooks/log/useLogQueue.ts b/packages/shared/src/hooks/log/useLogQueue.ts index a43dad4058..17d7320e27 100644 --- a/packages/shared/src/hooks/log/useLogQueue.ts +++ b/packages/shared/src/hooks/log/useLogQueue.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import { MutableRefObject, useMemo, useRef } from 'react'; +import { MutableRefObject, useCallback, useRef } from 'react'; import { apiUrl } from '../../lib/config'; import useDebounceFn from '../useDebounceFn'; import { ExtensionMessageType } from '../../lib/extension'; @@ -56,49 +56,56 @@ export default function useLogQueue({ } }, 500); - return useMemo( - () => ({ - pushToQueue: (events) => { - queueRef.current.push(...events); - if (enabledRef.current) { - debouncedSendEvents(); - } - }, - setEnabled: (enabled) => { - enabledRef.current = enabled; - if (enabled && queueRef.current.length) { - debouncedSendEvents(); - } - }, - queueRef, - sendBeacon: () => { - if (queueRef.current.length) { - const events = queueRef.current; - queueRef.current = []; - const blob = new Blob([JSON.stringify({ events })], { - type: 'application/json', - }); - if (backgroundMethod) { - backgroundMethod?.({ - url: LOG_ENDPOINT, - type: ExtensionMessageType.FetchRequest, - args: { - body: JSON.stringify({ events }), - credentials: 'include', - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - }, - }); - } else { - navigator.sendBeacon(LOG_ENDPOINT, blob); - } - } - }, - }), - // @NOTE see https://dailydotdev.atlassian.net/l/cp/dK9h1zoM - // eslint-disable-next-line react-hooks/exhaustive-deps - [queueRef, debouncedSendEvents, enabledRef], + const pushToQueue = useCallback( + (events) => { + queueRef.current.push(...events); + if (enabledRef.current) { + debouncedSendEvents(); + } + }, + [debouncedSendEvents], ); + + const sendBeacon = useCallback(() => { + if (queueRef.current.length) { + const events = queueRef.current; + queueRef.current = []; + const blob = new Blob([JSON.stringify({ events })], { + type: 'application/json', + }); + if (backgroundMethod) { + backgroundMethod?.({ + url: LOG_ENDPOINT, + type: ExtensionMessageType.FetchRequest, + args: { + body: JSON.stringify({ events }), + credentials: 'include', + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + }, + }); + } else { + navigator.sendBeacon(LOG_ENDPOINT, blob); + } + } + }, [backgroundMethod]); + + const setEnabled = useCallback( + (enabled: boolean) => { + enabledRef.current = enabled; + if (enabled && queueRef.current.length) { + debouncedSendEvents(); + } + }, + [debouncedSendEvents], + ); + + return { + pushToQueue, + setEnabled, + queueRef, + sendBeacon, + }; } diff --git a/packages/shared/src/hooks/referral/useJoinReferral.spec.tsx b/packages/shared/src/hooks/referral/useJoinReferral.spec.tsx index 0142e6d71a..9f9bf55878 100644 --- a/packages/shared/src/hooks/referral/useJoinReferral.spec.tsx +++ b/packages/shared/src/hooks/referral/useJoinReferral.spec.tsx @@ -20,7 +20,7 @@ describe('useJoinReferral hook', () => { updateUser={jest.fn()} tokenRefreshed={false} isFetched - firstLoad={false} + isAuthReady > {children}