From 53647b30378989a60adff5b1bc467a069383585b Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 6 May 2024 11:20:44 -0600 Subject: [PATCH 01/23] refactor: Use async launch darkly --- apps/web/src/Providers.tsx | 57 +++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 8b9a0b1eed9..bbd955820c4 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,8 +1,9 @@ +import { Loader } from '@mantine/core'; import { CONTEXT_PATH, LAUNCH_DARKLY_CLIENT_SIDE_ID, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { withLDProvider } from 'launchdarkly-react-client-sdk'; -import { PropsWithChildren } from 'react'; +import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; +import { PropsWithChildren, useEffect, useRef, useState } from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { BrowserRouter } from 'react-router-dom'; import { api } from './api/api.client'; @@ -22,28 +23,46 @@ const queryClient = new QueryClient({ }, }); +type GenericProvider = ({ children }: { children: React.ReactNode }) => JSX.Element; + /** * Centralized Provider hierarchy. */ const Providers: React.FC> = ({ children }) => { + const LDProvider = useRef((props) => <>{props.children}); + const [isLDReady, setIsLDReady] = useState(false); + + useEffect(() => { + const fetchLDProvider = async () => { + LDProvider.current = await asyncWithLDProvider({ + clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, + reactOptions: { + useCamelCaseFlagKeys: false, + }, + deferInitialization: true, + }); + setIsLDReady(true); + }; + fetchLDProvider(); + }); + + if (!isLDReady) { + return ; + } + return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); }; -export default Sentry.withProfiler( - withLDProvider({ - clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, - reactOptions: { - useCamelCaseFlagKeys: false, - }, - })(Providers) -); +export default Sentry.withProfiler(Providers); From 4200cd7188559a0e0982956ab4239732b1fb9390 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 6 May 2024 21:37:19 -0600 Subject: [PATCH 02/23] refactor: LaunchDarklyProvider --- apps/web/src/LaunchDarklyProvider.tsx | 94 +++++++++++++++++++ apps/web/src/Providers.tsx | 52 +++------- .../shared-web/src/hooks/useAuthController.ts | 5 +- .../shared-web/src/providers/AuthProvider.tsx | 1 - 4 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 apps/web/src/LaunchDarklyProvider.tsx diff --git a/apps/web/src/LaunchDarklyProvider.tsx b/apps/web/src/LaunchDarklyProvider.tsx new file mode 100644 index 00000000000..1d360474fb3 --- /dev/null +++ b/apps/web/src/LaunchDarklyProvider.tsx @@ -0,0 +1,94 @@ +import { Loader } from '@mantine/core'; +import { LoadingOverlay } from '@novu/design-system'; +import { IOrganizationEntity } from '@novu/shared'; +import { LAUNCH_DARKLY_CLIENT_SIDE_ID, useAuthContext, useFeatureFlags } from '@novu/shared-web'; +import { asyncWithLDProvider, useLDClient } from 'launchdarkly-react-client-sdk'; +import { PropsWithChildren, useEffect, useRef, useState } from 'react'; + +type GenericProvider = ({ children }: { children: React.ReactNode }) => JSX.Element; +const DEFAULT_GENERIC_PROVIDER: GenericProvider = (props) => <>{props.children}; + +export interface ILaunchDarklyProviderProps { + organization?: IOrganizationEntity; +} + +/** + * @requires AuthProvider must be wrapped in the AuthProvider. + */ +export const LaunchDarklyProvider: React.FC> = ({ children }) => { + const LDProvider = useRef(DEFAULT_GENERIC_PROVIDER); + const [isLDReady, setIsLDReady] = useState(false); + + const authContext = useAuthContext(); + if (!authContext) { + throw new Error('LaunchDarklyProvider must be used within AuthProvider!'); + } + const { currentOrganization } = authContext; + // const ldClient = useFeatureFlags(); + // eslint-disable-next-line multiline-comment-style + // useEffect(() => { + // console.log({ org: authContext.currentOrganization, ldClient }); + // if (!authContext.currentOrganization || !ldClient) { + // return; + // } + // console.log('Reidentify', authContext.currentOrganization); + // ldClient.identify({ + // kind: 'organization', + // key: authContext.currentOrganization._id, + // name: authContext.currentOrganization.name, + // }); + // }, [authContext.currentOrganization, ldClient]); + + useEffect(() => { + const fetchLDProvider = async () => { + if (!currentOrganization) { + return; + } + + LDProvider.current = await asyncWithLDProvider({ + clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, + context: { + kind: 'organization', + key: currentOrganization._id, + name: currentOrganization.name, + }, + reactOptions: { + useCamelCaseFlagKeys: false, + }, + // deferInitialization: true, + }); + setIsLDReady(true); + }; + fetchLDProvider(); + }, [setIsLDReady, currentOrganization]); + + /** + * Current issues: + * - This breaks login since there's no org -- can we match against "isUnprotectedUrl"? + * - + */ + + // eslint-disable-next-line multiline-comment-style + // if (!isLDReady) { + // return ( + // + // <> + // + // ); + // } + + return ( + + {children} + + ); +}; + +/** + * Refreshes feature flags on org change using the LaunchDarkly client from the provider. + */ +function LaunchDarklyClientWrapper({ children, org }: PropsWithChildren<{ org?: IOrganizationEntity }>) { + useFeatureFlags(org); + + return <>{children}; +} diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index bbd955820c4..7a8db136079 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,13 +1,12 @@ -import { Loader } from '@mantine/core'; -import { CONTEXT_PATH, LAUNCH_DARKLY_CLIENT_SIDE_ID, SegmentProvider } from '@novu/shared-web'; +import { CONTEXT_PATH, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; -import { PropsWithChildren, useEffect, useRef, useState } from 'react'; +import { PropsWithChildren } from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { BrowserRouter } from 'react-router-dom'; import { api } from './api/api.client'; import { AuthProvider } from './components/providers/AuthProvider'; +import { LaunchDarklyProvider } from './LaunchDarklyProvider'; const defaultQueryFn = async ({ queryKey }: { queryKey: string }) => { const response = await api.get(`${queryKey[0]}`); @@ -23,45 +22,22 @@ const queryClient = new QueryClient({ }, }); -type GenericProvider = ({ children }: { children: React.ReactNode }) => JSX.Element; - /** * Centralized Provider hierarchy. */ const Providers: React.FC> = ({ children }) => { - const LDProvider = useRef((props) => <>{props.children}); - const [isLDReady, setIsLDReady] = useState(false); - - useEffect(() => { - const fetchLDProvider = async () => { - LDProvider.current = await asyncWithLDProvider({ - clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, - reactOptions: { - useCamelCaseFlagKeys: false, - }, - deferInitialization: true, - }); - setIsLDReady(true); - }; - fetchLDProvider(); - }); - - if (!isLDReady) { - return ; - } - return ( - - - - - - {children} - - - - - + + + + + + {children} + + + + + ); }; diff --git a/libs/shared-web/src/hooks/useAuthController.ts b/libs/shared-web/src/hooks/useAuthController.ts index a76f86c5b43..2c006214d67 100644 --- a/libs/shared-web/src/hooks/useAuthController.ts +++ b/libs/shared-web/src/hooks/useAuthController.ts @@ -1,7 +1,6 @@ import { useEffect, useCallback, useState } from 'react'; import axios from 'axios'; import jwtDecode from 'jwt-decode'; -import { useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Sentry from '@sentry/react'; import type { IJwtPayload, IOrganizationEntity, IUserEntity } from '@novu/shared'; @@ -41,7 +40,6 @@ export function getToken(): string { export function useAuthController() { const segment = useSegment(); const queryClient = useQueryClient(); - const navigate = useNavigate(); const [token, setToken] = useState(() => { const initialToken = getToken(); applyToken(initialToken); @@ -122,7 +120,8 @@ export function useAuthController() { const logout = () => { setTokenCallback(null); queryClient.clear(); - navigate('/auth/login'); + // avoid usage of react-router here to prevent needing AuthProvider to be wrapped in the BrowserRouter + window.location.assign('/auth/login'); segment.reset(); }; diff --git a/libs/shared-web/src/providers/AuthProvider.tsx b/libs/shared-web/src/providers/AuthProvider.tsx index 04c72b2cc72..04d7312ebc4 100644 --- a/libs/shared-web/src/providers/AuthProvider.tsx +++ b/libs/shared-web/src/providers/AuthProvider.tsx @@ -28,7 +28,6 @@ export const useAuthContext = (): UserContext => useContext(AuthContext); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const { token, setToken, user, organization, isUserLoading, logout, jwtPayload, organizations } = useAuthController(); - useFeatureFlags(organization); return ( Date: Tue, 7 May 2024 11:27:44 -0600 Subject: [PATCH 03/23] refactor: LD Provider with checks --- apps/web/src/LaunchDarklyProvider.tsx | 69 ++++++++----------- apps/web/src/Providers.tsx | 21 +++++- libs/shared-web/src/constants/index.ts | 1 + .../src/constants/unprotected-routes.const.ts | 12 ++++ libs/shared-web/src/hooks/useFeatureFlags.ts | 2 +- libs/shared-web/src/index.ts | 1 + .../src/utils/checkIsUnprotectedPathname.ts | 5 ++ libs/shared-web/src/utils/index.ts | 1 + 8 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 libs/shared-web/src/constants/unprotected-routes.const.ts create mode 100644 libs/shared-web/src/utils/checkIsUnprotectedPathname.ts diff --git a/apps/web/src/LaunchDarklyProvider.tsx b/apps/web/src/LaunchDarklyProvider.tsx index 1d360474fb3..fa557d40e85 100644 --- a/apps/web/src/LaunchDarklyProvider.tsx +++ b/apps/web/src/LaunchDarklyProvider.tsx @@ -1,43 +1,41 @@ -import { Loader } from '@mantine/core'; -import { LoadingOverlay } from '@novu/design-system'; import { IOrganizationEntity } from '@novu/shared'; -import { LAUNCH_DARKLY_CLIENT_SIDE_ID, useAuthContext, useFeatureFlags } from '@novu/shared-web'; -import { asyncWithLDProvider, useLDClient } from 'launchdarkly-react-client-sdk'; -import { PropsWithChildren, useEffect, useRef, useState } from 'react'; +import { + LAUNCH_DARKLY_CLIENT_SIDE_ID, + useAuthContext, + useFeatureFlags, + checkIsUnprotectedPathname, +} from '@novu/shared-web'; +import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; +import { PropsWithChildren, ReactNode, useEffect, useRef, useState } from 'react'; -type GenericProvider = ({ children }: { children: React.ReactNode }) => JSX.Element; -const DEFAULT_GENERIC_PROVIDER: GenericProvider = (props) => <>{props.children}; +/** A provider with children required */ +type GenericLDProvider = Awaited>; + +/** Simply renders the children */ +const DEFAULT_GENERIC_PROVIDER: GenericLDProvider = ({ children }) => <>{children}; export interface ILaunchDarklyProviderProps { - organization?: IOrganizationEntity; + /** Renders when LaunchDarkly is enabled and is awaiting initialization */ + fallbackDisplay: ReactNode; } /** + * Async provider for feature flags. + * * @requires AuthProvider must be wrapped in the AuthProvider. */ -export const LaunchDarklyProvider: React.FC> = ({ children }) => { - const LDProvider = useRef(DEFAULT_GENERIC_PROVIDER); +export const LaunchDarklyProvider: React.FC> = ({ + children, + fallbackDisplay, +}) => { + const LDProvider = useRef(DEFAULT_GENERIC_PROVIDER); const [isLDReady, setIsLDReady] = useState(false); const authContext = useAuthContext(); if (!authContext) { - throw new Error('LaunchDarklyProvider must be used within AuthProvider!'); + throw new Error('LaunchDarklyProvider must be used within !'); } const { currentOrganization } = authContext; - // const ldClient = useFeatureFlags(); - // eslint-disable-next-line multiline-comment-style - // useEffect(() => { - // console.log({ org: authContext.currentOrganization, ldClient }); - // if (!authContext.currentOrganization || !ldClient) { - // return; - // } - // console.log('Reidentify', authContext.currentOrganization); - // ldClient.identify({ - // kind: 'organization', - // key: authContext.currentOrganization._id, - // name: authContext.currentOrganization.name, - // }); - // }, [authContext.currentOrganization, ldClient]); useEffect(() => { const fetchLDProvider = async () => { @@ -55,27 +53,16 @@ export const LaunchDarklyProvider: React.FC - // <> - // - // ); - // } + if (shouldUseLaunchDarkly() && !checkIsUnprotectedPathname(window.location.pathname) && !isLDReady) { + return <>{fallbackDisplay}; + } return ( @@ -92,3 +79,7 @@ function LaunchDarklyClientWrapper({ children, org }: PropsWithChildren<{ org?: return <>{children}; } + +function shouldUseLaunchDarkly(): boolean { + return !!process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; +} diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 7a8db136079..3363d18f48d 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,3 +1,4 @@ +import { Loader } from '@mantine/core'; import { CONTEXT_PATH, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -7,6 +8,7 @@ import { BrowserRouter } from 'react-router-dom'; import { api } from './api/api.client'; import { AuthProvider } from './components/providers/AuthProvider'; import { LaunchDarklyProvider } from './LaunchDarklyProvider'; +import { css } from './styled-system/css'; const defaultQueryFn = async ({ queryKey }: { queryKey: string }) => { const response = await api.get(`${queryKey[0]}`); @@ -22,6 +24,23 @@ const queryClient = new QueryClient({ }, }); +const fallbackDisplay = ( +
+ +
+); + /** * Centralized Provider hierarchy. */ @@ -30,7 +49,7 @@ const Providers: React.FC> = ({ children }) => { - + {children} diff --git a/libs/shared-web/src/constants/index.ts b/libs/shared-web/src/constants/index.ts index aa06998c968..75e665a2e00 100644 --- a/libs/shared-web/src/constants/index.ts +++ b/libs/shared-web/src/constants/index.ts @@ -1,2 +1,3 @@ export * from './routes.enum'; export * from './BaseEnvironmentEnum'; +export * from './unprotected-routes.const'; diff --git a/libs/shared-web/src/constants/unprotected-routes.const.ts b/libs/shared-web/src/constants/unprotected-routes.const.ts new file mode 100644 index 00000000000..77f95518a8d --- /dev/null +++ b/libs/shared-web/src/constants/unprotected-routes.const.ts @@ -0,0 +1,12 @@ +import { ROUTES } from './routes.enum'; + +export const UNPROTECTED_ROUTES: ROUTES[] = [ + ROUTES.AUTH_SIGNUP, + ROUTES.AUTH_LOGIN, + ROUTES.AUTH_RESET_REQUEST, + ROUTES.AUTH_RESET_TOKEN, + ROUTES.AUTH_INVITATION_TOKEN, + ROUTES.AUTH_APPLICATION, +]; + +export const UNPROTECTED_ROUTES_SET: Set = new Set(UNPROTECTED_ROUTES); diff --git a/libs/shared-web/src/hooks/useFeatureFlags.ts b/libs/shared-web/src/hooks/useFeatureFlags.ts index 488b0441eef..afafa2b7ac5 100644 --- a/libs/shared-web/src/hooks/useFeatureFlags.ts +++ b/libs/shared-web/src/hooks/useFeatureFlags.ts @@ -5,7 +5,7 @@ import { useEffect } from 'react'; import { FEATURE_FLAGS } from '../config'; -export const useFeatureFlags = (organization: IOrganizationEntity) => { +export const useFeatureFlags = (organization?: IOrganizationEntity) => { const ldClient = useLDClient(); useEffect(() => { diff --git a/libs/shared-web/src/index.ts b/libs/shared-web/src/index.ts index c903bad34fb..493e3ce358d 100644 --- a/libs/shared-web/src/index.ts +++ b/libs/shared-web/src/index.ts @@ -5,3 +5,4 @@ export * from './hooks'; export * from './providers'; export * from './constants'; export * from './components'; +export * from './utils'; diff --git a/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts b/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts new file mode 100644 index 00000000000..57fec771c4f --- /dev/null +++ b/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts @@ -0,0 +1,5 @@ +import { UNPROTECTED_ROUTES_SET } from '../constants'; + +export const checkIsUnprotectedPathname = (curPathname: string): boolean => { + return UNPROTECTED_ROUTES_SET.has(curPathname); +}; diff --git a/libs/shared-web/src/utils/index.ts b/libs/shared-web/src/utils/index.ts index 18c52ee5ae1..94cf7e378d0 100644 --- a/libs/shared-web/src/utils/index.ts +++ b/libs/shared-web/src/utils/index.ts @@ -1 +1,2 @@ export * from './segment'; +export * from './checkIsUnprotectedPathname'; From 0e7da6ab41baba3a7d4e34bd411d8cca60adad33 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Tue, 7 May 2024 11:35:01 -0600 Subject: [PATCH 04/23] refactor: Extract LaunchDarklyProvider to shared-web --- apps/web/src/Providers.tsx | 4 ++-- .../src/providers}/LaunchDarklyProvider.tsx | 21 ++++++++----------- libs/shared-web/src/providers/index.ts | 1 + .../src/utils/checkShouldUseLaunchDarkly.ts | 3 +++ libs/shared-web/src/utils/index.ts | 1 + 5 files changed, 16 insertions(+), 14 deletions(-) rename {apps/web/src => libs/shared-web/src/providers}/LaunchDarklyProvider.tsx (80%) create mode 100644 libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 3363d18f48d..04b26dd7968 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,5 +1,5 @@ import { Loader } from '@mantine/core'; -import { CONTEXT_PATH, SegmentProvider } from '@novu/shared-web'; +import { CONTEXT_PATH, LaunchDarklyProvider, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PropsWithChildren } from 'react'; @@ -7,7 +7,6 @@ import { HelmetProvider } from 'react-helmet-async'; import { BrowserRouter } from 'react-router-dom'; import { api } from './api/api.client'; import { AuthProvider } from './components/providers/AuthProvider'; -import { LaunchDarklyProvider } from './LaunchDarklyProvider'; import { css } from './styled-system/css'; const defaultQueryFn = async ({ queryKey }: { queryKey: string }) => { @@ -24,6 +23,7 @@ const queryClient = new QueryClient({ }, }); +/** Full-page loader that uses color-preferences for background */ const fallbackDisplay = (
>; @@ -59,8 +57,11 @@ export const LaunchDarklyProvider: React.FC{fallbackDisplay}; } @@ -79,7 +80,3 @@ function LaunchDarklyClientWrapper({ children, org }: PropsWithChildren<{ org?: return <>{children}; } - -function shouldUseLaunchDarkly(): boolean { - return !!process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; -} diff --git a/libs/shared-web/src/providers/index.ts b/libs/shared-web/src/providers/index.ts index 38f270699df..d844de645cf 100644 --- a/libs/shared-web/src/providers/index.ts +++ b/libs/shared-web/src/providers/index.ts @@ -1,2 +1,3 @@ export * from './SegmentProvider'; export * from './AuthProvider'; +export * from './LaunchDarklyProvider'; diff --git a/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts new file mode 100644 index 00000000000..4f6be93d4ef --- /dev/null +++ b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts @@ -0,0 +1,3 @@ +export const checkShouldUseLaunchDarkly = (): boolean => { + return !!process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; +}; diff --git a/libs/shared-web/src/utils/index.ts b/libs/shared-web/src/utils/index.ts index 94cf7e378d0..6adc6c71ce1 100644 --- a/libs/shared-web/src/utils/index.ts +++ b/libs/shared-web/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './segment'; export * from './checkIsUnprotectedPathname'; +export * from './checkShouldUseLaunchDarkly'; From 938916541e6b5da93b4c8352c5688c383c8fa01f Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Tue, 7 May 2024 11:36:03 -0600 Subject: [PATCH 05/23] fix: Don't show /brand page when IA is enabled --- apps/web/src/AppRoutes.tsx | 11 +++++++---- apps/web/src/SettingsRoutes.tsx | 2 -- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/web/src/AppRoutes.tsx b/apps/web/src/AppRoutes.tsx index 0b5fb6b2b50..309b62c9d80 100644 --- a/apps/web/src/AppRoutes.tsx +++ b/apps/web/src/AppRoutes.tsx @@ -49,6 +49,7 @@ import { useSettingsRoutes } from './SettingsRoutes'; export const AppRoutes = () => { const isImprovedOnboardingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_ONBOARDING_ENABLED); + const isInformationArchitectureEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_INFORMATION_ARCHITECTURE_ENABLED); return ( @@ -116,10 +117,12 @@ export const AppRoutes = () => { } /> } /> } /> - }> - } /> - } /> - + {!isInformationArchitectureEnabled && ( + }> + } /> + } /> + + )} }> } /> diff --git a/apps/web/src/SettingsRoutes.tsx b/apps/web/src/SettingsRoutes.tsx index 1d5c75e513a..228fd5b3701 100644 --- a/apps/web/src/SettingsRoutes.tsx +++ b/apps/web/src/SettingsRoutes.tsx @@ -6,7 +6,6 @@ import { ProductLead } from './components/utils/ProductLead'; import { ROUTES } from './constants/routes.enum'; import { useFeatureFlag } from './hooks'; import { BillingRoutes } from './pages/BillingPages'; -import { BrandingForm as BrandingFormOld } from './pages/brand/tabs'; import { BrandingPage } from './pages/brand/tabs/v2'; import { MembersInvitePage as MembersInvitePageNew } from './pages/invites/v2/MembersInvitePage'; import { AccessSecurityPage, ApiKeysPage, BillingPage, TeamPage, UserProfilePage } from './pages/settings'; @@ -50,7 +49,6 @@ export const useSettingsRoutes = () => { } /> } /> } /> - } /> Date: Tue, 7 May 2024 12:11:35 -0600 Subject: [PATCH 06/23] feat: Update loader --- apps/web/src/Providers.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 04b26dd7968..6016e1ed9ad 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,4 +1,5 @@ import { Loader } from '@mantine/core'; +import { colors } from '@novu/design-system'; import { CONTEXT_PATH, LaunchDarklyProvider, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -37,7 +38,7 @@ const fallbackDisplay = ( _osLight: { bg: 'legacy.BGLight' }, })} > - +
); From e992ffd90303dc1588346e8b02b34d3c8e1deb80 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Tue, 7 May 2024 12:46:54 -0600 Subject: [PATCH 07/23] fix: Avoid any calls to LD if disabled --- libs/shared-web/src/hooks/useFeatureFlags.ts | 12 +++++++----- .../src/providers/LaunchDarklyProvider.tsx | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/libs/shared-web/src/hooks/useFeatureFlags.ts b/libs/shared-web/src/hooks/useFeatureFlags.ts index afafa2b7ac5..8fc30303392 100644 --- a/libs/shared-web/src/hooks/useFeatureFlags.ts +++ b/libs/shared-web/src/hooks/useFeatureFlags.ts @@ -1,7 +1,7 @@ import { FeatureFlagsKeysEnum, IOrganizationEntity, prepareBooleanStringFeatureFlag } from '@novu/shared'; -import { useFlags } from 'launchdarkly-react-client-sdk'; -import { useLDClient } from 'launchdarkly-react-client-sdk'; +import { useFlags, useLDClient } from 'launchdarkly-react-client-sdk'; import { useEffect } from 'react'; +import { checkShouldUseLaunchDarkly } from '../utils'; import { FEATURE_FLAGS } from '../config'; @@ -9,7 +9,7 @@ export const useFeatureFlags = (organization?: IOrganizationEntity) => { const ldClient = useLDClient(); useEffect(() => { - if (!organization?._id) { + if (!checkShouldUseLaunchDarkly() || !organization?._id) { return; } @@ -24,10 +24,12 @@ export const useFeatureFlags = (organization?: IOrganizationEntity) => { }; export const useFeatureFlag = (key: FeatureFlagsKeysEnum): boolean => { - const { [key]: featureFlag } = useFlags(); + /** We knowingly break the rule of hooks here to avoid making any LaunchDarkly calls when it is disabled */ + // eslint-disable-next-line + const flagValue = checkShouldUseLaunchDarkly() ? useFlags()[key] : undefined; const fallbackValue = false; const value = FEATURE_FLAGS[key]; const defaultValue = prepareBooleanStringFeatureFlag(value, fallbackValue); - return featureFlag ?? defaultValue; + return flagValue ?? defaultValue; }; diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx index 7c75942b7de..e25b2fff8d6 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx @@ -36,11 +36,12 @@ export const LaunchDarklyProvider: React.FC { - const fetchLDProvider = async () => { - if (!currentOrganization) { - return; - } + // no need to fetch if LD is disabled or there isn't an org to query against + if (!checkShouldUseLaunchDarkly() || !currentOrganization) { + return; + } + const fetchLDProvider = async () => { LDProvider.current = await asyncWithLDProvider({ clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, context: { @@ -54,6 +55,7 @@ export const LaunchDarklyProvider: React.FC Date: Tue, 7 May 2024 13:44:24 -0600 Subject: [PATCH 08/23] refactor: Use auth state instead of routes! --- libs/shared-web/src/constants/index.ts | 1 - .../src/constants/unprotected-routes.const.ts | 12 ------------ libs/shared-web/src/providers/AuthProvider.tsx | 8 ++++++-- .../src/providers/LaunchDarklyProvider.tsx | 8 ++++---- .../src/utils/checkIsUnprotectedPathname.ts | 5 ----- libs/shared-web/src/utils/index.ts | 1 - 6 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 libs/shared-web/src/constants/unprotected-routes.const.ts delete mode 100644 libs/shared-web/src/utils/checkIsUnprotectedPathname.ts diff --git a/libs/shared-web/src/constants/index.ts b/libs/shared-web/src/constants/index.ts index 75e665a2e00..aa06998c968 100644 --- a/libs/shared-web/src/constants/index.ts +++ b/libs/shared-web/src/constants/index.ts @@ -1,3 +1,2 @@ export * from './routes.enum'; export * from './BaseEnvironmentEnum'; -export * from './unprotected-routes.const'; diff --git a/libs/shared-web/src/constants/unprotected-routes.const.ts b/libs/shared-web/src/constants/unprotected-routes.const.ts deleted file mode 100644 index 77f95518a8d..00000000000 --- a/libs/shared-web/src/constants/unprotected-routes.const.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ROUTES } from './routes.enum'; - -export const UNPROTECTED_ROUTES: ROUTES[] = [ - ROUTES.AUTH_SIGNUP, - ROUTES.AUTH_LOGIN, - ROUTES.AUTH_RESET_REQUEST, - ROUTES.AUTH_RESET_TOKEN, - ROUTES.AUTH_INVITATION_TOKEN, - ROUTES.AUTH_APPLICATION, -]; - -export const UNPROTECTED_ROUTES_SET: Set = new Set(UNPROTECTED_ROUTES); diff --git a/libs/shared-web/src/providers/AuthProvider.tsx b/libs/shared-web/src/providers/AuthProvider.tsx index 04d7312ebc4..6b954533fe6 100644 --- a/libs/shared-web/src/providers/AuthProvider.tsx +++ b/libs/shared-web/src/providers/AuthProvider.tsx @@ -1,9 +1,10 @@ import React, { useContext } from 'react'; import { IOrganizationEntity, IUserEntity, IJwtPayload } from '@novu/shared'; -import { useAuthController, useFeatureFlags } from '../hooks'; +import { useAuthController } from '../hooks'; type UserContext = { token: string | null; + isLoggedIn: boolean; currentUser: IUserEntity | undefined; isUserLoading: boolean; currentOrganization: IOrganizationEntity | undefined; @@ -15,6 +16,7 @@ type UserContext = { const AuthContext = React.createContext({ token: null, + isLoggedIn: false, currentUser: undefined, isUserLoading: true, setToken: undefined as any, @@ -27,12 +29,14 @@ const AuthContext = React.createContext({ export const useAuthContext = (): UserContext => useContext(AuthContext); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - const { token, setToken, user, organization, isUserLoading, logout, jwtPayload, organizations } = useAuthController(); + const { token, setToken, user, organization, isUserLoading, logout, jwtPayload, organizations, isLoggedIn } = + useAuthController(); return ( !'); } - const { currentOrganization } = authContext; + const { currentOrganization, isLoggedIn } = authContext; useEffect(() => { // no need to fetch if LD is disabled or there isn't an org to query against @@ -61,9 +61,9 @@ export const LaunchDarklyProvider: React.FC{fallbackDisplay}; } diff --git a/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts b/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts deleted file mode 100644 index 57fec771c4f..00000000000 --- a/libs/shared-web/src/utils/checkIsUnprotectedPathname.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { UNPROTECTED_ROUTES_SET } from '../constants'; - -export const checkIsUnprotectedPathname = (curPathname: string): boolean => { - return UNPROTECTED_ROUTES_SET.has(curPathname); -}; diff --git a/libs/shared-web/src/utils/index.ts b/libs/shared-web/src/utils/index.ts index 6adc6c71ce1..a8a6e27eb8a 100644 --- a/libs/shared-web/src/utils/index.ts +++ b/libs/shared-web/src/utils/index.ts @@ -1,3 +1,2 @@ export * from './segment'; -export * from './checkIsUnprotectedPathname'; export * from './checkShouldUseLaunchDarkly'; From 961a3346c64eb4cd0f147d2a8946e74f588df371 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 09:39:18 -0700 Subject: [PATCH 09/23] fix: Use config for env var --- libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts index 4f6be93d4ef..19f653189f2 100644 --- a/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts +++ b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts @@ -1,3 +1,5 @@ +import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; + export const checkShouldUseLaunchDarkly = (): boolean => { - return !!process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; + return !!LAUNCH_DARKLY_CLIENT_SIDE_ID; }; From 00bf34eaee36f90c9c6ad5963eb82de55622e8a8 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 16:11:42 -0700 Subject: [PATCH 10/23] refactor: Export UserContext --- libs/shared-web/src/providers/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared-web/src/providers/AuthProvider.tsx b/libs/shared-web/src/providers/AuthProvider.tsx index 6b954533fe6..4534bf64587 100644 --- a/libs/shared-web/src/providers/AuthProvider.tsx +++ b/libs/shared-web/src/providers/AuthProvider.tsx @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import { IOrganizationEntity, IUserEntity, IJwtPayload } from '@novu/shared'; import { useAuthController } from '../hooks'; -type UserContext = { +export type UserContext = { token: string | null; isLoggedIn: boolean; currentUser: IUserEntity | undefined; From a34a72b6f02eee049aa6265e9f598350cea1b10d Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 16:13:29 -0700 Subject: [PATCH 11/23] refactor: Use utils for determining LD init --- libs/shared-web/src/hooks/useFeatureFlags.ts | 4 +- .../src/providers/LaunchDarklyProvider.tsx | 97 +++++++++++++++---- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/libs/shared-web/src/hooks/useFeatureFlags.ts b/libs/shared-web/src/hooks/useFeatureFlags.ts index 8fc30303392..e828e67f352 100644 --- a/libs/shared-web/src/hooks/useFeatureFlags.ts +++ b/libs/shared-web/src/hooks/useFeatureFlags.ts @@ -9,11 +9,11 @@ export const useFeatureFlags = (organization?: IOrganizationEntity) => { const ldClient = useLDClient(); useEffect(() => { - if (!checkShouldUseLaunchDarkly() || !organization?._id) { + if (!checkShouldUseLaunchDarkly() || !organization?._id || !ldClient) { return; } - ldClient?.identify({ + ldClient.identify({ kind: 'organization', key: organization._id, name: organization.name, diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx index 8967be545de..506800f2cb8 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx @@ -1,10 +1,10 @@ -import { IOrganizationEntity } from '@novu/shared'; +import { IOrganizationEntity, IUserEntity } from '@novu/shared'; import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; -import { PropsWithChildren, ReactNode, useEffect, useRef, useState } from 'react'; +import { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; import { useFeatureFlags } from '../hooks'; import { checkShouldUseLaunchDarkly } from '../utils'; -import { useAuthContext } from './AuthProvider'; +import { useAuthContext, UserContext } from './AuthProvider'; /** A provider with children required */ type GenericLDProvider = Awaited>; @@ -33,37 +33,53 @@ export const LaunchDarklyProvider: React.FC!'); } - const { currentOrganization, isLoggedIn } = authContext; + const { currentOrganization, isLoggedIn, isUserLoading, currentUser } = authContext; + + const { shouldWaitForLd, doesNeedOrg } = useMemo(() => checkShouldInitializeLaunchDarkly(authContext), [authContext]); useEffect(() => { - // no need to fetch if LD is disabled or there isn't an org to query against - if (!checkShouldUseLaunchDarkly() || !currentOrganization) { + if (!shouldWaitForLd) { return; } const fetchLDProvider = async () => { - LDProvider.current = await asyncWithLDProvider({ - clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, - context: { - kind: 'organization', - key: currentOrganization._id, - name: currentOrganization.name, - }, - reactOptions: { - useCamelCaseFlagKeys: false, - }, - }); - setIsLDReady(true); + try { + LDProvider.current = await asyncWithLDProvider({ + clientSideID: LAUNCH_DARKLY_CLIENT_SIDE_ID, + reactOptions: { + useCamelCaseFlagKeys: false, + }, + // determine which context to use based on if an organization is available + context: currentOrganization + ? { + kind: 'organization', + key: currentOrganization._id, + name: currentOrganization.name, + } + : { + /** + * When user is not authenticated, assigns an id to them to ensure consistent results. + * https://docs.launchdarkly.com/sdk/features/anonymous#javascript + */ + kind: 'user', + anonymous: true, + }, + }); + } catch (err: unknown) { + // FIXME: what should we do here since we don't have logging? + } finally { + setIsLDReady(true); + } }; fetchLDProvider(); - }, [setIsLDReady, currentOrganization]); + }, [setIsLDReady, shouldWaitForLd, currentOrganization]); /** * For self-hosted, LD will not be enabled, so do not block initialization. * Must not show the fallback if the user isn't logged-in to avoid issues with un-authenticated routes (i.e. login). */ - if (checkShouldUseLaunchDarkly() && isLoggedIn && !isLDReady) { + if ((shouldWaitForLd || (doesNeedOrg && !currentOrganization)) && !isLDReady) { return <>{fallbackDisplay}; } @@ -82,3 +98,44 @@ function LaunchDarklyClientWrapper({ children, org }: PropsWithChildren<{ org?: return <>{children}; } + +function checkShouldInitializeLaunchDarkly(userCtx: UserContext): { shouldWaitForLd: boolean; doesNeedOrg?: boolean } { + const { isLoggedIn, currentOrganization } = userCtx; + + if (!checkShouldUseLaunchDarkly()) { + return { shouldWaitForLd: false }; + } + + // enable feature flags for unauthenticated areas of the app + if (!isLoggedIn) { + return { shouldWaitForLd: true }; + } + + // user must be loaded -- a user can have `isLoggedIn` true when `currentUser` is undefined + // eslint-disable-next-line + // if (!currentUser) { + // return { shouldWaitForLd: false }; + // } + + // allow LD to load when the user is created but still in onboarding + const isUserFullyRegistered = checkIsUserFullyRegistered(userCtx); + if (!isUserFullyRegistered) { + return { shouldWaitForLd: true }; + } + + // if a user is fully on-boarded, but no organization has loaded, we must wait for the organization to initialize the client. + return { shouldWaitForLd: !!currentOrganization, doesNeedOrg: true }; +} + +/** + * Determine if a user is fully-registered; if not, they're still in onboarding. + */ +function checkIsUserFullyRegistered(userCtx: UserContext): boolean { + /* + * Determine if the user has completed registration based on if they have an associated orgId. + * Use jobTitle as a back-up + */ + const isUserFullyRegistered = !!userCtx.jwtPayload?.organizationId || !!userCtx.currentUser?.jobTitle; + + return isUserFullyRegistered; +} From 953339e94534a3d46ab0b4aaef8c2f2e4a97ec37 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 22:50:13 -0700 Subject: [PATCH 12/23] refactor: Extract util --- .../src/providers/LaunchDarklyProvider.tsx | 18 +++--------------- .../src/utils/auth-selectors/index.ts | 1 + .../selectHasUserCompletedSignUp.ts | 13 +++++++++++++ 3 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 libs/shared-web/src/utils/auth-selectors/index.ts create mode 100644 libs/shared-web/src/utils/auth-selectors/selectHasUserCompletedSignUp.ts diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx index 506800f2cb8..32f26aa47ff 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx @@ -1,6 +1,7 @@ -import { IOrganizationEntity, IUserEntity } from '@novu/shared'; +import { IOrganizationEntity } from '@novu/shared'; import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; import { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import { selectHasUserCompletedSignUp } from '../utils/auth-selectors'; import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; import { useFeatureFlags } from '../hooks'; import { checkShouldUseLaunchDarkly } from '../utils'; @@ -118,7 +119,7 @@ function checkShouldInitializeLaunchDarkly(userCtx: UserContext): { shouldWaitFo // } // allow LD to load when the user is created but still in onboarding - const isUserFullyRegistered = checkIsUserFullyRegistered(userCtx); + const isUserFullyRegistered = selectHasUserCompletedSignUp(userCtx); if (!isUserFullyRegistered) { return { shouldWaitForLd: true }; } @@ -126,16 +127,3 @@ function checkShouldInitializeLaunchDarkly(userCtx: UserContext): { shouldWaitFo // if a user is fully on-boarded, but no organization has loaded, we must wait for the organization to initialize the client. return { shouldWaitForLd: !!currentOrganization, doesNeedOrg: true }; } - -/** - * Determine if a user is fully-registered; if not, they're still in onboarding. - */ -function checkIsUserFullyRegistered(userCtx: UserContext): boolean { - /* - * Determine if the user has completed registration based on if they have an associated orgId. - * Use jobTitle as a back-up - */ - const isUserFullyRegistered = !!userCtx.jwtPayload?.organizationId || !!userCtx.currentUser?.jobTitle; - - return isUserFullyRegistered; -} diff --git a/libs/shared-web/src/utils/auth-selectors/index.ts b/libs/shared-web/src/utils/auth-selectors/index.ts new file mode 100644 index 00000000000..53240e5334f --- /dev/null +++ b/libs/shared-web/src/utils/auth-selectors/index.ts @@ -0,0 +1 @@ +export * from './selectHasUserCompletedSignUp'; diff --git a/libs/shared-web/src/utils/auth-selectors/selectHasUserCompletedSignUp.ts b/libs/shared-web/src/utils/auth-selectors/selectHasUserCompletedSignUp.ts new file mode 100644 index 00000000000..31900f917cf --- /dev/null +++ b/libs/shared-web/src/utils/auth-selectors/selectHasUserCompletedSignUp.ts @@ -0,0 +1,13 @@ +import { UserContext } from '../../providers'; + +/** + * Determine if a user is fully-registered; if not, they're still in onboarding. + */ +export const selectHasUserCompletedSignUp = (userCtx: UserContext): boolean => { + if (!userCtx) { + return false; + } + + // User has completed registration if they have an associated orgId. + return !!userCtx.jwtPayload?.organizationId; +}; From 037d94600458894c4664ffcecea288c9092f078f Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 23:38:59 -0700 Subject: [PATCH 13/23] refactor: Extract selectors --- .../src/providers/LaunchDarklyProvider.tsx | 43 +++---------------- .../src/utils/auth-selectors/index.ts | 2 + .../selectShouldInitializeLaunchDarkly.tsx | 25 +++++++++++ .../selectShouldShowLaunchDarklyFallback.tsx | 25 +++++++++++ 4 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx create mode 100644 libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx index 32f26aa47ff..522ab21c7b0 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx @@ -1,11 +1,10 @@ import { IOrganizationEntity } from '@novu/shared'; import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; import { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; -import { selectHasUserCompletedSignUp } from '../utils/auth-selectors'; import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; import { useFeatureFlags } from '../hooks'; -import { checkShouldUseLaunchDarkly } from '../utils'; -import { useAuthContext, UserContext } from './AuthProvider'; +import { useAuthContext } from './AuthProvider'; +import { selectShouldShowLaunchDarklyFallback, selectShouldInitializeLaunchDarkly } from '../utils/auth-selectors'; /** A provider with children required */ type GenericLDProvider = Awaited>; @@ -34,12 +33,12 @@ export const LaunchDarklyProvider: React.FC!'); } - const { currentOrganization, isLoggedIn, isUserLoading, currentUser } = authContext; + const { currentOrganization } = authContext; - const { shouldWaitForLd, doesNeedOrg } = useMemo(() => checkShouldInitializeLaunchDarkly(authContext), [authContext]); + const shouldInitializeLd = useMemo(() => selectShouldInitializeLaunchDarkly(authContext), [authContext]); useEffect(() => { - if (!shouldWaitForLd) { + if (!shouldInitializeLd) { return; } @@ -74,13 +73,13 @@ export const LaunchDarklyProvider: React.FC{fallbackDisplay}; } @@ -99,31 +98,3 @@ function LaunchDarklyClientWrapper({ children, org }: PropsWithChildren<{ org?: return <>{children}; } - -function checkShouldInitializeLaunchDarkly(userCtx: UserContext): { shouldWaitForLd: boolean; doesNeedOrg?: boolean } { - const { isLoggedIn, currentOrganization } = userCtx; - - if (!checkShouldUseLaunchDarkly()) { - return { shouldWaitForLd: false }; - } - - // enable feature flags for unauthenticated areas of the app - if (!isLoggedIn) { - return { shouldWaitForLd: true }; - } - - // user must be loaded -- a user can have `isLoggedIn` true when `currentUser` is undefined - // eslint-disable-next-line - // if (!currentUser) { - // return { shouldWaitForLd: false }; - // } - - // allow LD to load when the user is created but still in onboarding - const isUserFullyRegistered = selectHasUserCompletedSignUp(userCtx); - if (!isUserFullyRegistered) { - return { shouldWaitForLd: true }; - } - - // if a user is fully on-boarded, but no organization has loaded, we must wait for the organization to initialize the client. - return { shouldWaitForLd: !!currentOrganization, doesNeedOrg: true }; -} diff --git a/libs/shared-web/src/utils/auth-selectors/index.ts b/libs/shared-web/src/utils/auth-selectors/index.ts index 53240e5334f..ca4767bd396 100644 --- a/libs/shared-web/src/utils/auth-selectors/index.ts +++ b/libs/shared-web/src/utils/auth-selectors/index.ts @@ -1 +1,3 @@ export * from './selectHasUserCompletedSignUp'; +export * from './selectShouldShowLaunchDarklyFallback'; +export * from './selectShouldInitializeLaunchDarkly'; diff --git a/libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx b/libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx new file mode 100644 index 00000000000..59d90e31a0a --- /dev/null +++ b/libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx @@ -0,0 +1,25 @@ +import { selectHasUserCompletedSignUp } from './selectHasUserCompletedSignUp'; +import { checkShouldUseLaunchDarkly } from '../checkShouldUseLaunchDarkly'; +import { UserContext } from '../../providers/AuthProvider'; + +/** Determine if LaunchDarkly should be initialized based on the current auth context */ +export function selectShouldInitializeLaunchDarkly(userCtx: UserContext): boolean { + const { isLoggedIn, currentOrganization } = userCtx; + // don't show fallback if LaunchDarkly isn't enabled + if (!checkShouldUseLaunchDarkly()) { + return false; + } + + // enable feature flags for unauthenticated areas of the app + if (!isLoggedIn) { + return true; + } + + // allow LD to load when the user is created but still in onboarding + if (!selectHasUserCompletedSignUp(userCtx)) { + return true; + } + + // if a user is fully on-boarded, but no organization has loaded, we must wait for the organization to initialize the client. + return !!currentOrganization; +} diff --git a/libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx b/libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx new file mode 100644 index 00000000000..96bb2f746d6 --- /dev/null +++ b/libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx @@ -0,0 +1,25 @@ +import { selectHasUserCompletedSignUp } from '.'; +import { checkShouldUseLaunchDarkly } from '..'; +import { UserContext } from '../../providers/AuthProvider'; + +/** Determine if a fallback should be shown instead of the provider-wrapped application */ +export function selectShouldShowLaunchDarklyFallback(userCtx: UserContext, isLDReady: boolean): boolean { + const { isLoggedIn, currentOrganization } = userCtx; + // don't show fallback if LaunchDarkly isn't enabled + if (!checkShouldUseLaunchDarkly()) { + return false; + } + + // don't show fallback for unauthenticated areas of the app + if (!isLoggedIn) { + return false; + } + + // don't show fallback if user is still in onboarding + if (!selectHasUserCompletedSignUp(userCtx)) { + return false; + } + + // if the organization is not loaded or we haven't loaded LD, show the fallback + return !currentOrganization || !isLDReady; +} From 59b0edba81310040f6280d21497e855d2968b096 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 23:48:23 -0700 Subject: [PATCH 14/23] fix: Hide new layouts route behind flag --- apps/web/src/AppRoutes.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/src/AppRoutes.tsx b/apps/web/src/AppRoutes.tsx index 309b62c9d80..514f9b219ab 100644 --- a/apps/web/src/AppRoutes.tsx +++ b/apps/web/src/AppRoutes.tsx @@ -117,15 +117,16 @@ export const AppRoutes = () => { } /> } /> } /> - {!isInformationArchitectureEnabled && ( + {!isInformationArchitectureEnabled ? ( }> } /> } /> + ) : ( + }> + } /> + )} - }> - } /> - } /> From c1cc6c9c286b221c57a1f53d30910079f2f02034 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Wed, 8 May 2024 23:58:55 -0700 Subject: [PATCH 15/23] refactor: Add comment + export --- libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts | 1 + libs/shared-web/src/utils/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts index 19f653189f2..6cc3a28f6f0 100644 --- a/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts +++ b/libs/shared-web/src/utils/checkShouldUseLaunchDarkly.ts @@ -1,5 +1,6 @@ import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; +/** Determine if client-side LaunchDarkly should be enabled */ export const checkShouldUseLaunchDarkly = (): boolean => { return !!LAUNCH_DARKLY_CLIENT_SIDE_ID; }; diff --git a/libs/shared-web/src/utils/index.ts b/libs/shared-web/src/utils/index.ts index a8a6e27eb8a..6cd450c1251 100644 --- a/libs/shared-web/src/utils/index.ts +++ b/libs/shared-web/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './segment'; export * from './checkShouldUseLaunchDarkly'; +export * from './auth-selectors'; From 30da61428b8493131562d1cfb51ba6d69a228c5e Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Thu, 9 May 2024 09:56:16 -0700 Subject: [PATCH 16/23] test: Auth spec --- apps/web/cypress/tests/auth.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/cypress/tests/auth.spec.ts b/apps/web/cypress/tests/auth.spec.ts index 2c7b61b8577..51e0e66051c 100644 --- a/apps/web/cypress/tests/auth.spec.ts +++ b/apps/web/cypress/tests/auth.spec.ts @@ -1,7 +1,10 @@ import * as capitalize from 'lodash.capitalize'; -import { JobTitleEnum, jobTitleToLabelMapper } from '@novu/shared'; +import { FeatureFlagsKeysEnum, JobTitleEnum, jobTitleToLabelMapper } from '@novu/shared'; describe('User Sign-up and Login', function () { + beforeEach(function () { + cy.mockFeatureFlags({ [FeatureFlagsKeysEnum.IS_INFORMATION_ARCHITECTURE_ENABLED]: false }); + }); describe('Sign up', function () { beforeEach(function () { cy.clearDatabase(); @@ -206,6 +209,7 @@ describe('User Sign-up and Login', function () { cy.clock(date); cy.visit('/subscribers'); + cy.waitLoadFeatureFlags(); // checking if token is removed from local storage cy.getLocalStorage('auth_token').should('be.null'); From 43273d9355c41098b76e78a4f7558b003e57ac3a Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Fri, 10 May 2024 14:10:56 -0700 Subject: [PATCH 17/23] test: Wait for feature flags --- apps/web/cypress/tests/auth.spec.ts | 61 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/web/cypress/tests/auth.spec.ts b/apps/web/cypress/tests/auth.spec.ts index 51e0e66051c..829ba9b21c3 100644 --- a/apps/web/cypress/tests/auth.spec.ts +++ b/apps/web/cypress/tests/auth.spec.ts @@ -13,7 +13,9 @@ describe('User Sign-up and Login', function () { it('should allow a visitor to sign-up, login, and logout', function () { cy.intercept('**/organization/**/switch').as('appSwitch'); - cy.visit('/auth/signup'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/signup'); + }); cy.getByTestId('fullName').type('Test User'); cy.getByTestId('email').type('example@example.com'); cy.getByTestId('password').type('usEr_password_123!'); @@ -33,7 +35,9 @@ describe('User Sign-up and Login', function () { }); it('should show account already exists when signing up with already registered mail', function () { - cy.visit('/auth/signup'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/signup'); + }); cy.getByTestId('fullName').type('Test User'); cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('usEr_password_123!'); @@ -43,7 +47,9 @@ describe('User Sign-up and Login', function () { }); it('should show invalid email error when signing up with invalid email', function () { - cy.visit('/auth/signup'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/signup'); + }); cy.getByTestId('fullName').type('Test User'); cy.getByTestId('email').type('test-user-1@example.c'); cy.getByTestId('password').type('usEr_password_123!'); @@ -57,7 +63,9 @@ describe('User Sign-up and Login', function () { if (!isCI) return; cy.intercept('**/organization/**/switch').as('appSwitch'); - cy.visit('/auth/signup'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/signup'); + }); cy.loginWithGitHub(); @@ -85,7 +93,9 @@ describe('User Sign-up and Login', function () { const gitHubUserEmail = Cypress.env('GITHUB_USER_EMAIL'); cy.intercept('**/organization/**/switch').as('appSwitch'); - cy.visit('/auth/signup'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/signup'); + }); cy.getByTestId('fullName').type('Test User'); cy.getByTestId('email').type(gitHubUserEmail); cy.getByTestId('password').type('usEr_password_123!'); @@ -118,13 +128,19 @@ describe('User Sign-up and Login', function () { }); it('should request a password reset flow', function () { - cy.visit('/auth/reset/request'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/reset/request'); + }); cy.getByTestId('email').type(this.session.user.email); cy.getByTestId('submit-btn').click(); cy.getByTestId('success-screen-reset').should('be.visible'); + cy.task('passwordResetToken', this.session.user._id).then((token) => { cy.visit('/auth/reset/' + token); }); + + // unfortunately there seems to be a timing issue in in which inputs are disabled + cy.wait(500); cy.getByTestId('password').type('A123e3e3e3!'); cy.getByTestId('password-repeat').focus().type('A123e3e3e3!'); @@ -140,18 +156,24 @@ describe('User Sign-up and Login', function () { it('should redirect to the dashboard page when a token exists in query', function () { cy.initializeSession({ disableLocalStorage: true }).then((session) => { - cy.visit('/auth/login?token=' + session.token); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login?token=' + session.token); + }); cy.location('pathname').should('equal', '/workflows'); }); }); it('should be redirect login with no auth', function () { - cy.visit('/'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/'); + }); cy.location('pathname').should('equal', '/auth/login'); }); it('should successfully login the user', function () { - cy.visit('/auth/login'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login'); + }); cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('123qwe!@#'); @@ -160,7 +182,9 @@ describe('User Sign-up and Login', function () { }); it('should show incorrect email or password error when authenticating with bad credentials', function () { - cy.visit('/auth/login'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login'); + }); cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('123456'); @@ -169,7 +193,9 @@ describe('User Sign-up and Login', function () { }); it('should show invalid email error when authenticating with invalid email', function () { - cy.visit('/auth/login'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login'); + }); cy.getByTestId('email').type('test-user-1@example.c'); cy.getByTestId('password').type('123456'); @@ -178,7 +204,9 @@ describe('User Sign-up and Login', function () { }); it('should show incorrect email or password error when authenticating with non-existing email', function () { - cy.visit('/auth/login'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login'); + }); cy.getByTestId('email').type('test-user-1@example.de'); cy.getByTestId('password').type('123456'); @@ -195,7 +223,9 @@ describe('User Sign-up and Login', function () { it('should logout user when auth token is expired', function () { // login the user - cy.visit('/auth/login'); + cy.waitLoadFeatureFlags(() => { + cy.visit('/auth/login'); + }); cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('123qwe!@#'); cy.getByTestId('submit-btn').click(); @@ -208,8 +238,9 @@ describe('User Sign-up and Login', function () { const date = new Date(Date.now() + THIRTY_DAYS + ONE_MINUTE); cy.clock(date); - cy.visit('/subscribers'); - cy.waitLoadFeatureFlags(); + cy.waitLoadFeatureFlags(() => { + cy.visit('/subscribers'); + }); // checking if token is removed from local storage cy.getLocalStorage('auth_token').should('be.null'); From 2ab55aba7ccbb8f7f646dfecf798e5909662bed3 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Sun, 12 May 2024 10:14:50 -0700 Subject: [PATCH 18/23] refactor: PR Feedback, small comments --- libs/shared-web/src/hooks/useAuthController.ts | 2 +- libs/shared-web/src/providers/LaunchDarklyProvider.tsx | 4 +++- .../selectShouldInitializeLaunchDarkly.tsx | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/libs/shared-web/src/hooks/useAuthController.ts b/libs/shared-web/src/hooks/useAuthController.ts index 2c006214d67..75ff5cc4125 100644 --- a/libs/shared-web/src/hooks/useAuthController.ts +++ b/libs/shared-web/src/hooks/useAuthController.ts @@ -121,7 +121,7 @@ export function useAuthController() { setTokenCallback(null); queryClient.clear(); // avoid usage of react-router here to prevent needing AuthProvider to be wrapped in the BrowserRouter - window.location.assign('/auth/login'); + window.location.replace('/auth/login'); segment.reset(); }; diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx index 522ab21c7b0..7cb6ddf3a21 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/libs/shared-web/src/providers/LaunchDarklyProvider.tsx @@ -1,3 +1,5 @@ +import * as Sentry from '@sentry/react'; + import { IOrganizationEntity } from '@novu/shared'; import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; import { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; @@ -66,7 +68,7 @@ export const LaunchDarklyProvider: React.FC Date: Sun, 12 May 2024 16:32:33 -0700 Subject: [PATCH 19/23] refactor: LD Provider in web with associated utils --- apps/web/src/Providers.tsx | 3 ++- .../src/components/launch-darkly}/LaunchDarklyProvider.tsx | 7 +++---- apps/web/src/components/launch-darkly/index.ts | 1 + .../utils}/selectShouldInitializeLaunchDarkly.tsx | 5 ++--- .../utils}/selectShouldShowLaunchDarklyFallback.tsx | 4 +--- libs/shared-web/src/hooks/useFeatureFlags.ts | 2 +- libs/shared-web/src/providers/index.ts | 1 - libs/shared-web/src/utils/auth-selectors/index.ts | 2 -- 8 files changed, 10 insertions(+), 15 deletions(-) rename {libs/shared-web/src/providers => apps/web/src/components/launch-darkly}/LaunchDarklyProvider.tsx (91%) create mode 100644 apps/web/src/components/launch-darkly/index.ts rename {libs/shared-web/src/utils/auth-selectors => apps/web/src/components/launch-darkly/utils}/selectShouldInitializeLaunchDarkly.tsx (84%) rename {libs/shared-web/src/utils/auth-selectors => apps/web/src/components/launch-darkly/utils}/selectShouldShowLaunchDarklyFallback.tsx (82%) diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 6016e1ed9ad..faabcada630 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -1,12 +1,13 @@ import { Loader } from '@mantine/core'; import { colors } from '@novu/design-system'; -import { CONTEXT_PATH, LaunchDarklyProvider, SegmentProvider } from '@novu/shared-web'; +import { CONTEXT_PATH, SegmentProvider } from '@novu/shared-web'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PropsWithChildren } from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { BrowserRouter } from 'react-router-dom'; import { api } from './api/api.client'; +import { LaunchDarklyProvider } from './components/launch-darkly'; import { AuthProvider } from './components/providers/AuthProvider'; import { css } from './styled-system/css'; diff --git a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx b/apps/web/src/components/launch-darkly/LaunchDarklyProvider.tsx similarity index 91% rename from libs/shared-web/src/providers/LaunchDarklyProvider.tsx rename to apps/web/src/components/launch-darkly/LaunchDarklyProvider.tsx index 7cb6ddf3a21..191348a5369 100644 --- a/libs/shared-web/src/providers/LaunchDarklyProvider.tsx +++ b/apps/web/src/components/launch-darkly/LaunchDarklyProvider.tsx @@ -3,10 +3,9 @@ import * as Sentry from '@sentry/react'; import { IOrganizationEntity } from '@novu/shared'; import { asyncWithLDProvider } from 'launchdarkly-react-client-sdk'; import { PropsWithChildren, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; -import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '../config'; -import { useFeatureFlags } from '../hooks'; -import { useAuthContext } from './AuthProvider'; -import { selectShouldShowLaunchDarklyFallback, selectShouldInitializeLaunchDarkly } from '../utils/auth-selectors'; +import { useFeatureFlags, useAuthContext, LAUNCH_DARKLY_CLIENT_SIDE_ID } from '@novu/shared-web'; +import { selectShouldInitializeLaunchDarkly } from './utils/selectShouldInitializeLaunchDarkly'; +import { selectShouldShowLaunchDarklyFallback } from './utils/selectShouldShowLaunchDarklyFallback'; /** A provider with children required */ type GenericLDProvider = Awaited>; diff --git a/apps/web/src/components/launch-darkly/index.ts b/apps/web/src/components/launch-darkly/index.ts new file mode 100644 index 00000000000..b1c5cfef1eb --- /dev/null +++ b/apps/web/src/components/launch-darkly/index.ts @@ -0,0 +1 @@ +export * from './LaunchDarklyProvider'; diff --git a/libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx b/apps/web/src/components/launch-darkly/utils/selectShouldInitializeLaunchDarkly.tsx similarity index 84% rename from libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx rename to apps/web/src/components/launch-darkly/utils/selectShouldInitializeLaunchDarkly.tsx index 8b692482c3e..d8131950cfd 100644 --- a/libs/shared-web/src/utils/auth-selectors/selectShouldInitializeLaunchDarkly.tsx +++ b/apps/web/src/components/launch-darkly/utils/selectShouldInitializeLaunchDarkly.tsx @@ -1,6 +1,5 @@ -import { selectHasUserCompletedSignUp } from './selectHasUserCompletedSignUp'; -import { checkShouldUseLaunchDarkly } from '../checkShouldUseLaunchDarkly'; -import { UserContext } from '../../providers/AuthProvider'; +import { selectHasUserCompletedSignUp, UserContext } from '@novu/shared-web'; +import { checkShouldUseLaunchDarkly } from '@novu/shared-web'; /** Determine if LaunchDarkly should be initialized based on the current auth context */ export function selectShouldInitializeLaunchDarkly(userCtx: UserContext): boolean { diff --git a/libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx b/apps/web/src/components/launch-darkly/utils/selectShouldShowLaunchDarklyFallback.tsx similarity index 82% rename from libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx rename to apps/web/src/components/launch-darkly/utils/selectShouldShowLaunchDarklyFallback.tsx index 96bb2f746d6..aa4e9839abe 100644 --- a/libs/shared-web/src/utils/auth-selectors/selectShouldShowLaunchDarklyFallback.tsx +++ b/apps/web/src/components/launch-darkly/utils/selectShouldShowLaunchDarklyFallback.tsx @@ -1,6 +1,4 @@ -import { selectHasUserCompletedSignUp } from '.'; -import { checkShouldUseLaunchDarkly } from '..'; -import { UserContext } from '../../providers/AuthProvider'; +import { selectHasUserCompletedSignUp, UserContext, checkShouldUseLaunchDarkly } from '@novu/shared-web'; /** Determine if a fallback should be shown instead of the provider-wrapped application */ export function selectShouldShowLaunchDarklyFallback(userCtx: UserContext, isLDReady: boolean): boolean { diff --git a/libs/shared-web/src/hooks/useFeatureFlags.ts b/libs/shared-web/src/hooks/useFeatureFlags.ts index e828e67f352..91a399480f4 100644 --- a/libs/shared-web/src/hooks/useFeatureFlags.ts +++ b/libs/shared-web/src/hooks/useFeatureFlags.ts @@ -1,7 +1,7 @@ import { FeatureFlagsKeysEnum, IOrganizationEntity, prepareBooleanStringFeatureFlag } from '@novu/shared'; import { useFlags, useLDClient } from 'launchdarkly-react-client-sdk'; import { useEffect } from 'react'; -import { checkShouldUseLaunchDarkly } from '../utils'; +import { checkShouldUseLaunchDarkly } from '../utils/checkShouldUseLaunchDarkly'; import { FEATURE_FLAGS } from '../config'; diff --git a/libs/shared-web/src/providers/index.ts b/libs/shared-web/src/providers/index.ts index d844de645cf..38f270699df 100644 --- a/libs/shared-web/src/providers/index.ts +++ b/libs/shared-web/src/providers/index.ts @@ -1,3 +1,2 @@ export * from './SegmentProvider'; export * from './AuthProvider'; -export * from './LaunchDarklyProvider'; diff --git a/libs/shared-web/src/utils/auth-selectors/index.ts b/libs/shared-web/src/utils/auth-selectors/index.ts index ca4767bd396..53240e5334f 100644 --- a/libs/shared-web/src/utils/auth-selectors/index.ts +++ b/libs/shared-web/src/utils/auth-selectors/index.ts @@ -1,3 +1 @@ export * from './selectHasUserCompletedSignUp'; -export * from './selectShouldShowLaunchDarklyFallback'; -export * from './selectShouldInitializeLaunchDarkly'; From 9bf9e28d4b06cb37213e679c4d53be0c0d37e657 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 13 May 2024 14:11:10 -0700 Subject: [PATCH 20/23] fix: Fix tests, re-order providers --- apps/web/cypress/tests/invites.spec.ts | 6 +++++- apps/web/src/Providers.tsx | 14 +++++++------- libs/shared-web/src/hooks/useAuthController.ts | 5 +++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/web/cypress/tests/invites.spec.ts b/apps/web/cypress/tests/invites.spec.ts index 54733a8c16c..3e84386a203 100644 --- a/apps/web/cypress/tests/invites.spec.ts +++ b/apps/web/cypress/tests/invites.spec.ts @@ -1,7 +1,9 @@ +import { FeatureFlagsKeysEnum } from '@novu/shared'; import * as capitalize from 'lodash.capitalize'; describe('Invites module', function () { beforeEach(function () { + cy.mockFeatureFlags({ [FeatureFlagsKeysEnum.IS_INFORMATION_ARCHITECTURE_ENABLED]: false }); cy.task('clearDatabase'); }); @@ -120,7 +122,9 @@ describe('Invites module', function () { cy.initializeSession().as('session'); const invitationPath = `/auth/invitation/${this.token}`; - cy.visit(invitationPath); + cy.waitLoadFeatureFlags(() => { + cy.visit(invitationPath); + }); cy.getByTestId('success-screen-reset').click(); // checking if token is removed from local storage diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index faabcada630..bdc47fd51f7 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -50,13 +50,13 @@ const Providers: React.FC> = ({ children }) => { return ( - - - - {children} - - - + + + + {children} + + + ); diff --git a/libs/shared-web/src/hooks/useAuthController.ts b/libs/shared-web/src/hooks/useAuthController.ts index 75ff5cc4125..bb41e6384bc 100644 --- a/libs/shared-web/src/hooks/useAuthController.ts +++ b/libs/shared-web/src/hooks/useAuthController.ts @@ -7,6 +7,7 @@ import type { IJwtPayload, IOrganizationEntity, IUserEntity } from '@novu/shared import { useSegment } from '../providers'; import { api } from '../api'; +import { useNavigate } from 'react-router-dom'; function getUser() { return api.get('/v1/users/me'); @@ -40,6 +41,7 @@ export function getToken(): string { export function useAuthController() { const segment = useSegment(); const queryClient = useQueryClient(); + const navigate = useNavigate(); const [token, setToken] = useState(() => { const initialToken = getToken(); applyToken(initialToken); @@ -120,8 +122,7 @@ export function useAuthController() { const logout = () => { setTokenCallback(null); queryClient.clear(); - // avoid usage of react-router here to prevent needing AuthProvider to be wrapped in the BrowserRouter - window.location.replace('/auth/login'); + navigate('/auth/login'); segment.reset(); }; From be3c6b91df64cfcb6a2a38a2570687f5b0e4b1af Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 13 May 2024 14:22:06 -0700 Subject: [PATCH 21/23] refactor: Update useAuthController.ts --- libs/shared-web/src/hooks/useAuthController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared-web/src/hooks/useAuthController.ts b/libs/shared-web/src/hooks/useAuthController.ts index bb41e6384bc..a76f86c5b43 100644 --- a/libs/shared-web/src/hooks/useAuthController.ts +++ b/libs/shared-web/src/hooks/useAuthController.ts @@ -1,13 +1,13 @@ import { useEffect, useCallback, useState } from 'react'; import axios from 'axios'; import jwtDecode from 'jwt-decode'; +import { useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Sentry from '@sentry/react'; import type { IJwtPayload, IOrganizationEntity, IUserEntity } from '@novu/shared'; import { useSegment } from '../providers'; import { api } from '../api'; -import { useNavigate } from 'react-router-dom'; function getUser() { return api.get('/v1/users/me'); From c5118890f12cb540b8d2ad8c5e998f1332ea8c19 Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 13 May 2024 14:25:16 -0700 Subject: [PATCH 22/23] fix: Clarify comment --- apps/web/src/SettingsRoutes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/SettingsRoutes.tsx b/apps/web/src/SettingsRoutes.tsx index 228fd5b3701..b469ccc6726 100644 --- a/apps/web/src/SettingsRoutes.tsx +++ b/apps/web/src/SettingsRoutes.tsx @@ -41,7 +41,7 @@ export const useSettingsRoutes = () => { ); } - /* TODO: remove all routes above once information architecture is fully enabled */ + /* TODO: remove all routes below once information architecture is fully enabled */ return ( <> }> From f155b3d51ce45ca11a319b3c01d5fc2718f6b28f Mon Sep 17 00:00:00 2001 From: Joel Anton Date: Mon, 13 May 2024 17:43:39 -0700 Subject: [PATCH 23/23] refactor: Move waits --- apps/web/cypress/tests/auth.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/cypress/tests/auth.spec.ts b/apps/web/cypress/tests/auth.spec.ts index 829ba9b21c3..67e20fef42f 100644 --- a/apps/web/cypress/tests/auth.spec.ts +++ b/apps/web/cypress/tests/auth.spec.ts @@ -223,13 +223,13 @@ describe('User Sign-up and Login', function () { it('should logout user when auth token is expired', function () { // login the user - cy.waitLoadFeatureFlags(() => { - cy.visit('/auth/login'); - }); + cy.visit('/auth/login'); cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('123qwe!@#'); cy.getByTestId('submit-btn').click(); + cy.waitLoadFeatureFlags(); + cy.location('pathname').should('equal', '/workflows'); // setting current time in future, to simulate expired token @@ -238,9 +238,9 @@ describe('User Sign-up and Login', function () { const date = new Date(Date.now() + THIRTY_DAYS + ONE_MINUTE); cy.clock(date); - cy.waitLoadFeatureFlags(() => { - cy.visit('/subscribers'); - }); + cy.visit('/subscribers'); + + cy.waitLoadFeatureFlags(); // checking if token is removed from local storage cy.getLocalStorage('auth_token').should('be.null');