Skip to content

Commit

Permalink
feat(dashboard,web): opt-in app switching and redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX committed Nov 14, 2024
1 parent c86094d commit 632709e
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 47 deletions.
9 changes: 3 additions & 6 deletions apps/dashboard/src/components/opt-in-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Dialog modal open={!isOptedIn} onOpenChange={optIn}>
<Dialog modal open={!!isFirstVisit} onOpenChange={updateNewDashboardFirstVisit}>
<DialogPortal>
<DialogOverlay />
<DialogContent className="p-0">
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/user-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ 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 (
<UserButton afterSignOutUrl={ROUTES.SIGN_IN}>
<UserButton.MenuItems>
<UserButton.Action
label="Go back to the legacy dashboard"
labelIcon={<RiSignpostFill size="16" color="var(--nv-colors-typography-text-main)" />}
onClick={redirectToLegacyDashboard}
onClick={optOut}
/>
</UserButton.MenuItems>
</UserButton>
Expand Down
18 changes: 18 additions & 0 deletions apps/dashboard/src/context/opt-in-provider.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
};
42 changes: 27 additions & 15 deletions apps/dashboard/src/hooks/use-new-dashboard-opt-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,62 @@ 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,
},
});
};

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,
};
}
9 changes: 6 additions & 3 deletions apps/dashboard/src/routes/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -30,9 +31,11 @@ const RootRouteInternal = () => {
<ClerkProvider>
<SegmentProvider>
<AuthProvider>
<HelmetProvider>
<Outlet />
</HelmetProvider>
<OptInProvider>
<HelmetProvider>
<Outlet />
</HelmetProvider>
</OptInProvider>
</AuthProvider>
</SegmentProvider>
</ClerkProvider>
Expand Down
17 changes: 10 additions & 7 deletions apps/web/src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]}`);
Expand All @@ -36,13 +37,15 @@ const Providers: React.FC<PropsWithChildren<{}>> = ({ children }) => {
<SegmentProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<EnvironmentProvider>
<HelmetProvider>
<StudioStateProvider>
<ContainerProvider>{children}</ContainerProvider>
</StudioStateProvider>
</HelmetProvider>
</EnvironmentProvider>
<OptInProvider>
<EnvironmentProvider>
<HelmetProvider>
<StudioStateProvider>
<ContainerProvider>{children}</ContainerProvider>
</StudioStateProvider>
</HelmetProvider>
</EnvironmentProvider>
</OptInProvider>
</AuthProvider>
</QueryClientProvider>
</SegmentProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -33,7 +32,7 @@ export function NewDashboardOptInWidget() {
</Text>
</div>
<div className={styles.buttonContainer}>
<Button size="sm" variant="transparent" onClick={redirectToNewDashboard}>
<Button size="sm" variant="transparent" onClick={optIn}>
Take me there
</Button>
</div>
Expand All @@ -44,6 +43,7 @@ export function NewDashboardOptInWidget() {
const styles = {
card: css({
padding: '9px 16px !important',
marginBottom: '16px',
backgroundColor: 'surface.popover !important',
_before: {
content: '""',
Expand Down
49 changes: 49 additions & 0 deletions apps/web/src/components/providers/OptInProvider.tsx
Original file line number Diff line number Diff line change
@@ -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}</_OptInProvider>;
}

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_<id>" 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}</>;
};
4 changes: 2 additions & 2 deletions apps/web/src/ee/clerk/components/UserProfileButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -16,7 +16,7 @@ export function UserProfileButton() {
<UserButton.Action
label="Try out the new dashboard (beta)"
labelIcon={<IconBolt size="16" color="var(--nv-colors-typography-text-main)" />}
onClick={redirectToNewDashboard}
onClick={optIn}
/>
</UserButton.MenuItems>
)}
Expand Down
15 changes: 9 additions & 6 deletions apps/web/src/hooks/useNewDashboardOptIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
};
Expand All @@ -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 = () => {
Expand All @@ -39,9 +42,9 @@ export function useNewDashboardOptIn() {
};

return {
isLoaded,
optIn,
dismiss,
status: getCurrentOptInStatus(),
redirectToNewDashboard,
};
}

0 comments on commit 632709e

Please sign in to comment.