From f10ab0e31695b18176e7b13ae87fef6540356bfa Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Wed, 25 Feb 2026 09:43:13 +0200 Subject: [PATCH 1/3] chore(shared): strict-fix core context providers --- packages/shared/src/contexts/AlertContext.tsx | 47 +++-- packages/shared/src/contexts/AuthContext.tsx | 53 ++++-- .../shared/src/contexts/BootProvider.spec.tsx | 34 +++- packages/shared/src/contexts/BootProvider.tsx | 81 +++++--- packages/shared/src/contexts/DndContext.tsx | 22 ++- packages/shared/src/contexts/FeedContext.tsx | 30 ++- .../src/contexts/NotificationsContext.tsx | 16 +- .../src/contexts/PushNotificationContext.tsx | 23 ++- .../shared/src/contexts/SettingsContext.tsx | 177 ++++++++++++++++-- .../src/contexts/SubscriptionContext.tsx | 16 +- .../shared/src/contexts/WritePostContext.tsx | 16 +- 11 files changed, 399 insertions(+), 116 deletions(-) diff --git a/packages/shared/src/contexts/AlertContext.tsx b/packages/shared/src/contexts/AlertContext.tsx index ab31232653..02bd9998eb 100644 --- a/packages/shared/src/contexts/AlertContext.tsx +++ b/packages/shared/src/contexts/AlertContext.tsx @@ -2,7 +2,7 @@ import type { ReactNode, ReactElement } from 'react'; import React, { useMemo, useContext } from 'react'; import type { UseMutateAsyncFunction } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import type { Alerts, AlertsUpdate } from '../graphql/alerts'; +import type { Alerts } from '../graphql/alerts'; import { UPDATE_ALERTS, UPDATE_LAST_BOOT_POPUP, @@ -13,30 +13,29 @@ import { gqlClient } from '../graphql/common'; export const ALERT_DEFAULTS: Alerts = { filter: true, - rankLastSeen: null, - myFeed: null, + rankLastSeen: undefined, + myFeed: undefined, squadTour: true, showGenericReferral: false, showStreakMilestone: false, showAchievementUnlock: null, - lastBootPopup: null, - briefBannerLastSeen: null, + lastBootPopup: undefined, + briefBannerLastSeen: undefined, }; export interface AlertContextData { alerts: Alerts; isFetched?: boolean; loadedAlerts?: boolean; - updateAlerts?: UseMutateAsyncFunction< + updateAlerts?: UseMutateAsyncFunction; + updateLastReferralReminder?: UseMutateAsyncFunction; + updateLastBootPopup?: UseMutateAsyncFunction; + updateHasSeenOpportunity?: UseMutateAsyncFunction< unknown, unknown, - AlertsUpdate, - () => Promise + boolean | void >; - updateLastReferralReminder?: UseMutateAsyncFunction; - updateLastBootPopup?: UseMutateAsyncFunction; - updateHasSeenOpportunity?: UseMutateAsyncFunction; - clearOpportunityAlert?: UseMutateAsyncFunction; + clearOpportunityAlert?: UseMutateAsyncFunction; } const AlertContext = React.createContext({ @@ -60,6 +59,16 @@ export const AlertContextProvider = ({ isFetched, updateAlerts, }: AlertContextProviderProps): ReactElement => { + const applyUpdatedAlerts = (updatedAlerts: Alerts): void => { + if (!updateAlerts) { + throw new Error( + 'updateAlerts callback is required in AlertContextProvider', + ); + } + + updateAlerts(updatedAlerts); + }; + const { mutateAsync: updateRemoteAlerts } = useMutation< unknown, unknown, @@ -69,15 +78,15 @@ export const AlertContextProvider = ({ gqlClient.request(UPDATE_ALERTS, { data: params, }), - onMutate: (params) => updateAlerts({ ...alerts, ...params }), + onMutate: (params) => applyUpdatedAlerts({ ...alerts, ...params }), onError: (_, params) => { - const rollback = Object.keys(params).reduce( + const rollback = (Object.keys(params) as Array).reduce( (values, key) => ({ ...values, [key]: alerts[key] }), - {}, + {} as Alerts, ); - updateAlerts({ ...alerts, ...rollback }); + applyUpdatedAlerts({ ...alerts, ...rollback }); }, }); @@ -89,16 +98,16 @@ export const AlertContextProvider = ({ mutationFn: () => gqlClient.request(UPDATE_LAST_BOOT_POPUP), onMutate: () => - updateAlerts({ + applyUpdatedAlerts({ ...alerts, lastBootPopup: new Date(), bootPopup: false, }), onError: () => { - updateAlerts({ + applyUpdatedAlerts({ ...alerts, - lastBootPopup: null, + lastBootPopup: undefined, bootPopup: true, }); }, diff --git a/packages/shared/src/contexts/AuthContext.tsx b/packages/shared/src/contexts/AuthContext.tsx index b9386a03d0..a85ce8b65a 100644 --- a/packages/shared/src/contexts/AuthContext.tsx +++ b/packages/shared/src/contexts/AuthContext.tsx @@ -51,7 +51,7 @@ export interface AuthContextData { shouldShowLogin: boolean; showLogin: ({ trigger, options }: ShowLoginParams) => void; closeLogin: () => void; - loginState?: LoginState; + loginState?: LoginState | null; logout: (reason: string) => Promise; updateUser: (user: LoggedUser) => Promise; loadingUser?: boolean; @@ -75,7 +75,24 @@ export interface AuthContextData { } const isExtension = checkIsExtension(); -const AuthContext = React.createContext(null); +const AuthContext = React.createContext({ + isLoggedIn: false, + shouldShowLogin: false, + showLogin: () => { + throw new Error('showLogin is not available outside AuthContextProvider'); + }, + closeLogin: () => { + throw new Error('closeLogin is not available outside AuthContextProvider'); + }, + logout: async () => { + throw new Error('logout is not available outside AuthContextProvider'); + }, + updateUser: async () => { + throw new Error('updateUser is not available outside AuthContextProvider'); + }, + tokenRefreshed: false, + getRedirectUri: () => '', +}); export const useAuthContext = (): AuthContextData => useContext(AuthContext); export default AuthContext; @@ -93,10 +110,11 @@ export const REGISTRATION_PATH = '/register'; export const logout = async (reason: string): Promise => { await dispatchLogout(reason); const params = getQueryParams(); - if (params.redirect_uri) { - window.location.replace(params.redirect_uri); + const redirectUri = params.redirect_uri; + if (redirectUri) { + window.location.replace(redirectUri); } else if (window.location.pathname === REGISTRATION_PATH) { - window.location.replace(process.env.NEXT_PUBLIC_WEBAPP_URL); + window.location.replace(process.env.NEXT_PUBLIC_WEBAPP_URL ?? '/'); } if (isExtension) { @@ -107,9 +125,15 @@ export const logout = async (reason: string): Promise => { }; export function checkIfGdprCovered(geo?: Boot['geo']): boolean { + const region = geo?.region; + + if (!region) { + return true; + } + return ( geo?.continent === Continent.Europe || - !outsideGdpr.includes(geo?.region) || + !outsideGdpr.includes(region) || isIOSNative() ); } @@ -152,7 +176,7 @@ export const AuthContextProvider = ({ isAndroidApp, }: AuthContextProviderProps): ReactElement => { const [loginState, setLoginState] = useState(null); - const endUser = user && 'providers' in user ? user : null; + const endUser = user && 'providers' in user ? user : undefined; const referral = user?.referralId || user?.referrer; const referralOrigin = user?.referralOrigin; const router = useRouter(); @@ -169,10 +193,15 @@ export const AuthContextProvider = ({ logout(LogoutReason.IncomleteOnboarding); } - const isValidRegion = useMemo( - () => !invalidPlusRegions.includes(geo?.region), - [geo?.region], - ); + const isValidRegion = useMemo(() => { + const region = geo?.region; + + if (!region) { + return true; + } + + return !invalidPlusRegions.includes(region); + }, [geo?.region]); return ( { const hasCompanion = !!isCompanionActivated(); diff --git a/packages/shared/src/contexts/BootProvider.spec.tsx b/packages/shared/src/contexts/BootProvider.spec.tsx index aba2441505..6fc794293c 100644 --- a/packages/shared/src/contexts/BootProvider.spec.tsx +++ b/packages/shared/src/contexts/BootProvider.spec.tsx @@ -53,7 +53,7 @@ beforeEach(() => { localStorage.clear(); }); -const defaultAlerts: Alerts = { filter: true, rankLastSeen: null }; +const defaultAlerts: Alerts = { filter: true, rankLastSeen: undefined }; const defaultSettings: RemoteSettings = { theme: 'bright', @@ -162,7 +162,13 @@ const SettingsMock = ({ Sidebar