From 632709e4058a54a23195f12fcb7322b967fe2b8d Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Thu, 14 Nov 2024 13:43:00 +0100 Subject: [PATCH] feat(dashboard,web): opt-in app switching and redirects --- .../dashboard/src/components/opt-in-modal.tsx | 9 ++-- .../dashboard/src/components/user-profile.tsx | 4 +- .../dashboard/src/context/opt-in-provider.tsx | 18 +++++++ .../src/hooks/use-new-dashboard-opt-in.ts | 42 ++++++++++------ apps/dashboard/src/routes/root.tsx | 9 ++-- apps/web/src/Providers.tsx | 17 ++++--- .../components/v2/NewDashboardOptInWidget.tsx | 12 ++--- .../components/providers/OptInProvider.tsx | 49 +++++++++++++++++++ .../ee/clerk/components/UserProfileButton.tsx | 4 +- apps/web/src/hooks/useNewDashboardOptIn.ts | 15 +++--- 10 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 apps/dashboard/src/context/opt-in-provider.tsx create mode 100644 apps/web/src/components/providers/OptInProvider.tsx diff --git a/apps/dashboard/src/components/opt-in-modal.tsx b/apps/dashboard/src/components/opt-in-modal.tsx index a8b6ce0fdc2..f9725e50a0d 100644 --- a/apps/dashboard/src/components/opt-in-modal.tsx +++ b/apps/dashboard/src/components/opt-in-modal.tsx @@ -13,19 +13,16 @@ import { } from '@/components/primitives/dialog'; import { RiCustomerService2Line } from 'react-icons/ri'; import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in'; -import { NewDashboardOptInStatusEnum } from '@novu/shared'; export const OptInModal = () => { - const { status, optIn } = useNewDashboardOptIn(); + const { isFirstVisit, updateNewDashboardFirstVisit } = useNewDashboardOptIn(); - const isOptedIn = status === NewDashboardOptInStatusEnum.OPTED_IN; - - if (isOptedIn) { + if (!isFirstVisit) { return null; } return ( - + diff --git a/apps/dashboard/src/components/user-profile.tsx b/apps/dashboard/src/components/user-profile.tsx index 48b2800745b..b8ff0045e4b 100644 --- a/apps/dashboard/src/components/user-profile.tsx +++ b/apps/dashboard/src/components/user-profile.tsx @@ -4,7 +4,7 @@ import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in'; import { RiSignpostFill } from 'react-icons/ri'; export function UserProfile() { - const { redirectToLegacyDashboard } = useNewDashboardOptIn(); + const { optOut } = useNewDashboardOptIn(); return ( @@ -12,7 +12,7 @@ export function UserProfile() { } - onClick={redirectToLegacyDashboard} + onClick={optOut} /> diff --git a/apps/dashboard/src/context/opt-in-provider.tsx b/apps/dashboard/src/context/opt-in-provider.tsx new file mode 100644 index 00000000000..d179f1a54f2 --- /dev/null +++ b/apps/dashboard/src/context/opt-in-provider.tsx @@ -0,0 +1,18 @@ +import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in'; +import { NewDashboardOptInStatusEnum } from '@novu/shared'; +import { PropsWithChildren, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export const OptInProvider = (props: PropsWithChildren) => { + const navigate = useNavigate(); + const { children } = props; + const { status, isLoaded } = useNewDashboardOptIn(); + + useEffect(() => { + if (isLoaded && status !== NewDashboardOptInStatusEnum.OPTED_IN) { + window.location.href = '/legacy/workflows'; + } + }, [status, navigate, isLoaded]); + + return <>{children}; +}; diff --git a/apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts b/apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts index 09223b304c7..38e7b5c010c 100644 --- a/apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts +++ b/apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts @@ -5,13 +5,13 @@ import { useUser } from '@clerk/clerk-react'; import { NewDashboardOptInStatusEnum } from '@novu/shared'; export function useNewDashboardOptIn() { - const { user } = useUser(); + const { user, isLoaded } = useUser(); const track = useTelemetry(); - const updateUserOptInStatus = (status: NewDashboardOptInStatusEnum) => { + const updateUserOptInStatus = async (status: NewDashboardOptInStatusEnum) => { if (!user) return; - user.update({ + await user.update({ unsafeMetadata: { ...user.unsafeMetadata, newDashboardOptInStatus: status, @@ -19,36 +19,48 @@ export function useNewDashboardOptIn() { }); }; + const updateNewDashboardFirstVisit = (firstVisit: boolean) => { + if (!user) return; + + user.update({ + unsafeMetadata: { + ...user.unsafeMetadata, + newDashboardFirstVisit: firstVisit, + }, + }); + }; + const getCurrentOptInStatus = () => { if (!user) return null; return user.unsafeMetadata?.newDashboardOptInStatus || null; }; - const redirectToLegacyDashboard = () => { - optOut(); + const getNewDashboardFirstVisit = () => { + if (!user) return false; + + return user.unsafeMetadata?.newDashboardFirstVisit || false; + }; + const redirectToLegacyDashboard = () => { if (NEW_DASHBOARD_FEEDBACK_FORM_URL) { window.open(NEW_DASHBOARD_FEEDBACK_FORM_URL, '_blank'); } - window.location.href = LEGACY_DASHBOARD_URL || window.location.origin + '/legacy'; - }; - - const optIn = () => { - track(TelemetryEvent.NEW_DASHBOARD_OPT_IN); - updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN); + window.location.href = LEGACY_DASHBOARD_URL || window.location.origin + '/legacy/workflows'; }; - const optOut = () => { + const optOut = async () => { track(TelemetryEvent.NEW_DASHBOARD_OPT_OUT); - updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_OUT); + await updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_OUT); + redirectToLegacyDashboard(); }; return { - optIn, + isLoaded, optOut, status: getCurrentOptInStatus(), - redirectToLegacyDashboard, + isFirstVisit: getNewDashboardFirstVisit(), + updateNewDashboardFirstVisit, }; } diff --git a/apps/dashboard/src/routes/root.tsx b/apps/dashboard/src/routes/root.tsx index adb94a52fe7..58ba1d52139 100644 --- a/apps/dashboard/src/routes/root.tsx +++ b/apps/dashboard/src/routes/root.tsx @@ -5,6 +5,7 @@ import { withProfiler, ErrorBoundary } from '@sentry/react'; import { SegmentProvider } from '@/context/segment'; import { AuthProvider } from '@/context/auth/auth-provider'; import { ClerkProvider } from '@/context/clerk-provider'; +import { OptInProvider } from '@/context/opt-in-provider'; const queryClient = new QueryClient(); @@ -30,9 +31,11 @@ const RootRouteInternal = () => { - - - + + + + + diff --git a/apps/web/src/Providers.tsx b/apps/web/src/Providers.tsx index 15f8065a271..07749d4cd66 100644 --- a/apps/web/src/Providers.tsx +++ b/apps/web/src/Providers.tsx @@ -11,6 +11,7 @@ import { EnvironmentProvider } from './components/providers/EnvironmentProvider' import { SegmentProvider } from './components/providers/SegmentProvider'; import { StudioStateProvider } from './studio/StudioStateProvider'; import { ContainerProvider } from './hooks/useContainer'; +import { OptInProvider } from './components/providers/OptInProvider'; const defaultQueryFn = async ({ queryKey }: { queryKey: string }) => { const response = await api.get(`${queryKey[0]}`); @@ -36,13 +37,15 @@ const Providers: React.FC> = ({ children }) => { - - - - {children} - - - + + + + + {children} + + + + diff --git a/apps/web/src/components/layout/components/v2/NewDashboardOptInWidget.tsx b/apps/web/src/components/layout/components/v2/NewDashboardOptInWidget.tsx index 6d79d95b02d..e878da46afd 100644 --- a/apps/web/src/components/layout/components/v2/NewDashboardOptInWidget.tsx +++ b/apps/web/src/components/layout/components/v2/NewDashboardOptInWidget.tsx @@ -2,20 +2,19 @@ import { Card } from '@mantine/core'; import { css } from '@novu/novui/css'; import { Text, Title, Button, IconButton } from '@novu/novui'; import { IconOutlineClose } from '@novu/novui/icons'; -import { FeatureFlagsKeysEnum, NewDashboardOptInStatusEnum } from '@novu/shared'; +import { FeatureFlagsKeysEnum } from '@novu/shared'; import { IS_SELF_HOSTED } from '../../../../config'; import { useFeatureFlag } from '../../../../hooks'; import { useNewDashboardOptIn } from '../../../../hooks/useNewDashboardOptIn'; export function NewDashboardOptInWidget() { - const { dismiss, redirectToNewDashboard, status } = useNewDashboardOptIn(); + const { dismiss, optIn, status } = useNewDashboardOptIn(); const isNewDashboardEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ENABLED); - const isDismissed = - status === NewDashboardOptInStatusEnum.DISMISSED || status === NewDashboardOptInStatusEnum.OPTED_OUT; + const showWidget = !status && isNewDashboardEnabled; - if (IS_SELF_HOSTED || isDismissed || !isNewDashboardEnabled) { + if (IS_SELF_HOSTED || !showWidget) { return null; } @@ -33,7 +32,7 @@ export function NewDashboardOptInWidget() {
-
@@ -44,6 +43,7 @@ export function NewDashboardOptInWidget() { const styles = { card: css({ padding: '9px 16px !important', + marginBottom: '16px', backgroundColor: 'surface.popover !important', _before: { content: '""', diff --git a/apps/web/src/components/providers/OptInProvider.tsx b/apps/web/src/components/providers/OptInProvider.tsx new file mode 100644 index 00000000000..d0fefe6e023 --- /dev/null +++ b/apps/web/src/components/providers/OptInProvider.tsx @@ -0,0 +1,49 @@ +import { FeatureFlagsKeysEnum, NewDashboardOptInStatusEnum } from '@novu/shared'; +import { PropsWithChildren, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useNewDashboardOptIn } from '../../hooks/useNewDashboardOptIn'; + +import { ROUTES } from '../../constants/routes'; +import { IS_EE_AUTH_ENABLED } from '../../config'; +import { useFeatureFlag } from '../../hooks'; + +const NEW_DASHBOARD_ROUTES = [ROUTES.WORKFLOWS]; + +export const OptInProvider = ({ children }: { children: React.ReactNode }) => { + const isNewDashboardEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ENABLED); + + if (IS_EE_AUTH_ENABLED && isNewDashboardEnabled) { + return <_OptInProvider>{children}; + } + + return <>{children}; +}; + +export const _OptInProvider = (props: PropsWithChildren) => { + const navigate = useNavigate(); + const { children } = props; + const { status, isLoaded } = useNewDashboardOptIn(); + + useEffect(() => { + if (isLoaded && status === NewDashboardOptInStatusEnum.OPTED_IN) { + const currentRoute = window.location.pathname.replace('/legacy', ''); + + /** + * if equivalent of current route (incl. subroutes) exits in new dashboard, redirect to it + * - /legacy/workflows -> /workflows + * - /legacy/workflows/edit/123 -> /workflows + */ + if (NEW_DASHBOARD_ROUTES.some((route) => currentRoute.includes(route))) { + /** + * TODO: in order to redirect to the same route, we need to translate the + * "dev_env_" or wf/step slugs to legacy environment id and vice-versa + * + * note: /legacy is part of public URL, so we can't navigate() outside of that + */ + window.location.href = window.location.origin; + } + } + }, [status, navigate, isLoaded]); + + return <>{children}; +}; diff --git a/apps/web/src/ee/clerk/components/UserProfileButton.tsx b/apps/web/src/ee/clerk/components/UserProfileButton.tsx index 09096525702..d0b89cbbfd0 100644 --- a/apps/web/src/ee/clerk/components/UserProfileButton.tsx +++ b/apps/web/src/ee/clerk/components/UserProfileButton.tsx @@ -6,7 +6,7 @@ import { useNewDashboardOptIn } from '../../../hooks/useNewDashboardOptIn'; import { useFeatureFlag } from '../../../hooks'; export function UserProfileButton() { - const { redirectToNewDashboard } = useNewDashboardOptIn(); + const { optIn } = useNewDashboardOptIn(); const isNewDashboardEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_NEW_DASHBOARD_ENABLED); return ( @@ -16,7 +16,7 @@ export function UserProfileButton() { } - onClick={redirectToNewDashboard} + onClick={optIn} /> )} diff --git a/apps/web/src/hooks/useNewDashboardOptIn.ts b/apps/web/src/hooks/useNewDashboardOptIn.ts index 0ad203d8f8d..5c8cc0205d1 100644 --- a/apps/web/src/hooks/useNewDashboardOptIn.ts +++ b/apps/web/src/hooks/useNewDashboardOptIn.ts @@ -4,16 +4,17 @@ import { NEW_DASHBOARD_URL } from '../config'; import { useSegment } from '../components/providers/SegmentProvider'; export function useNewDashboardOptIn() { - const { user } = useUser(); + const { user, isLoaded } = useUser(); const segment = useSegment(); - const updateUserOptInStatus = (status: NewDashboardOptInStatusEnum) => { + const updateUserOptInStatus = async (status: NewDashboardOptInStatusEnum) => { if (!user) return; - user.update({ + await user.update({ unsafeMetadata: { ...user.unsafeMetadata, newDashboardOptInStatus: status, + newDashboardFirstVisit: true, }, }); }; @@ -28,9 +29,11 @@ export function useNewDashboardOptIn() { window.location.href = NEW_DASHBOARD_URL || window.location.origin; }; - const optIn = () => { + const optIn = async () => { segment.track('New dashboard opt-in'); - updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN); + await updateUserOptInStatus(NewDashboardOptInStatusEnum.OPTED_IN); + localStorage.setItem('mantine-theme', 'light'); + redirectToNewDashboard(); }; const dismiss = () => { @@ -39,9 +42,9 @@ export function useNewDashboardOptIn() { }; return { + isLoaded, optIn, dismiss, status: getCurrentOptInStatus(), - redirectToNewDashboard, }; }