Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard,web): opt-in app switching and redirects #7002

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using navigate() here won't work since its external url

}
}, [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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now we redirect to the default page which is the workflows page - otherwise we would need to parse old env id to new env slugs to redirect to env/<env_slug>/workflows - will do in separate PR if there is a hard requirement to keep env id

}
}
}, [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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be set directly in local storage otherwise when using hook the operation seems to be async and can't be awaited

redirectToNewDashboard();
};

const dismiss = () => {
Expand All @@ -39,9 +42,9 @@ export function useNewDashboardOptIn() {
};

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