Skip to content

Commit

Permalink
chore(web): Crate with auth fixes (#6123)
Browse files Browse the repository at this point in the history
Co-authored-by: Denis Kralj <[email protected]>
  • Loading branch information
SokratisVidros and denis-kralj-novu authored Jul 23, 2024
1 parent b0d360e commit 6d96ea5
Show file tree
Hide file tree
Showing 46 changed files with 475 additions and 508 deletions.
2 changes: 1 addition & 1 deletion apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineConfig({
trace: 'on-first-retry',
permissions: ['clipboard-read'],
},
timeout: 30_000,
timeout: process.env.CI ? 30_000 : 60_000,
expect: {
timeout: 15000,
},
Expand Down
39 changes: 25 additions & 14 deletions apps/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,39 @@
padding: 0px;
box-sizing: border-box;
}
}

#loader {
position: absolute;
inset: 0;
z-index: 2147483647;

display: grid;
place-items: center;

@media (prefers-color-scheme: light) {
body {
/* surface.page (light mode) */
background-color: #EDF0F2;
}
/* surface.page (light mode) */
background-color: #EDF0F2;
}

@media (prefers-color-scheme: dark) {
body {
/* surface.page (dark mode) */
background-color: #13131A ;
}
/* surface.page (dark mode) */
background-color: #13131A ;
}
}

#loader {
display: grid;
width: 100dvw;
height: 100dvh;
place-items: center;
.fade-in {
opacity: 1;
transition: opacity 0.5s ease-out;
}

.fade-out {
opacity: 0;

> svg {
display: none;
}
}
</style>
</head>
<body>
Expand All @@ -90,7 +101,7 @@
<!-- End Google Tag Manager (noscript) -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- Instant loader -->
<div id="loader">
<div id="loader" class="fade-in">
<svg viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#ef3f5a" width="64px" role="presentation">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height" begin="0.5s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"></animate>
Expand Down
55 changes: 45 additions & 10 deletions apps/web/src/ApplicationReadyGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
import { Navigate, useLocation } from 'react-router-dom';
import { type PropsWithChildren, useLayoutEffect } from 'react';
import { useAuth, useEnvironment } from './hooks';
import { isStudioRoute } from './studio/utils/routing';
import { useAuth, useEnvironment, useMonitoring, useRouteScopes } from './hooks';
import { ROUTES } from './constants/routes';
import { IS_EE_AUTH_ENABLED } from './config/index';

export function ApplicationReadyGuard({ children }: PropsWithChildren<{}>) {
useMonitoring();
const location = useLocation();
const { isLoading: isLoadingAuth, inPublicRoute } = useAuth();
const { isLoading: isLoadingEnvironment } = useEnvironment();
const { inPublicRoute, inStudioRoute } = useRouteScopes();
const { isUserLoaded, isOrganizationLoaded, currentUser, currentOrganization } = useAuth();
const { isLoaded: isEnvironmentLoaded } = useEnvironment();

const isLoading = isStudioRoute(location.pathname) ? isLoadingAuth : isLoadingAuth && isLoadingEnvironment;
const isLoaded = isUserLoaded && isOrganizationLoaded && isEnvironmentLoaded;

// Clean up the skeleton loader when the app is ready
useLayoutEffect(() => {
if (inPublicRoute || !isLoading) {
document.getElementById('loader')?.remove();
if (inPublicRoute || inStudioRoute || isLoaded) {
const el = document.getElementById('loader');

if (!el) {
return;
}

/*
* Playwright doesn't always trigger transitionend, so we are using setTimeout
* instead to remove the skeleton loader at the end of the animation.
*/
setTimeout(() => el.remove(), 550);
requestAnimationFrame(() => el.classList.add('fade-out'));
}
}, [inPublicRoute, inStudioRoute, isLoaded]);

function isOnboardingComplete() {
if (IS_EE_AUTH_ENABLED) {
// TODO: replace with actual check property (e.g. isOnboardingCompleted)
return currentOrganization?.productUseCases !== undefined;
}
}, [inPublicRoute, isLoading]);

if (inPublicRoute || !isLoading) {
return currentOrganization;
}

if (inPublicRoute || inStudioRoute) {
return <>{children}</>;
}

return null;
if (!isLoaded) {
return null;
}

if (!currentUser && location.pathname !== ROUTES.AUTH_LOGIN) {
return <Navigate to={ROUTES.AUTH_LOGIN} replace />;
}

if (!isOnboardingComplete() && location.pathname !== ROUTES.AUTH_APPLICATION) {
return <Navigate to={ROUTES.AUTH_APPLICATION} replace />;
}

return <>{children}</>;
}
2 changes: 1 addition & 1 deletion apps/web/src/api/api.client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios';
import { CustomDataType } from '@novu/shared';
import { API_ROOT } from '../config';
import { getToken } from '../auth/getToken';
import { getToken } from '../components/providers/AuthProvider';
import { getEnvironmentId } from '../components/providers/EnvironmentProvider';

interface IOptions {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/api/hooks/useUserOnboardingStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface IRequestPayload {
}

export const useUserOnboardingStatus = () => {
const { currentUser, isLoading } = useAuth();
const { currentUser, isUserLoaded } = useAuth();
const queryClient = useQueryClient();

const mutationFunction = ({ showOnboarding }) => updateUserOnBoarding(showOnboarding);
Expand All @@ -34,7 +34,7 @@ export const useUserOnboardingStatus = () => {

return {
updateOnboardingStatus: updateOnBoardingStatus,
isLoading: isLoading || isLoadingUpdate,
isLoading: !isUserLoaded || isLoadingUpdate,
showOnboarding: shouldShowOnboarding(currentUser?.showOnBoarding),
};
};
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/api/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type UpdateOrgBrandingPayloadType = {
contentBackgroundValue?: string;
};

export function getOrganization() {
return api.get(`/v1/organizations/me`);
}

export function getOrganizations() {
return api.get(`/v1/organizations`);
}
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/auth/auth.const.ts

This file was deleted.

9 changes: 0 additions & 9 deletions apps/web/src/auth/getToken.ts

This file was deleted.

9 changes: 0 additions & 9 deletions apps/web/src/auth/getTokenClaims.ts

This file was deleted.

This file was deleted.

97 changes: 52 additions & 45 deletions apps/web/src/components/layout/components/PrivatePageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { useEffect, useMemo, useState } from 'react';
import { ErrorBoundary } from '@sentry/react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { Outlet, useLocation } from 'react-router-dom';
import styled from '@emotion/styled';

import { IntercomProvider } from 'react-use-intercom';
import { BRIDGE_SYNC_SAMPLE_ENDPOINT, BRIDGE_ENDPOINTS_LEGACY_VERSIONS, INTERCOM_APP_ID } from '../../../config';
import { EnsureOnboardingComplete } from './EnsureOnboardingComplete';
import { SpotLight } from '../../utils/Spotlight';
import { SpotLightProvider } from '../../providers/SpotlightProvider';
import { useEnvironment } from '../../../hooks';
import { useEnvironment, useRedirectURL, useRouteScopes } from '../../../hooks';
// TODO: Move sidebar under layout folder as it belongs here
import { Sidebar } from '../../nav/Sidebar';
import { HeaderNav } from './v2/HeaderNav';
import { FreeTrialBanner } from './FreeTrialBanner';
import { css } from '@novu/novui/css';
import { EnvironmentEnum } from '../../../studio/constants/EnvironmentEnum';
import { isStudioRoute } from '../../../studio/utils/routing';
import { SampleModeBanner } from './v2/SampleWorkflowsBanner';

const AppShell = styled.div`
Expand All @@ -36,15 +33,16 @@ export function PrivatePageLayout() {
const [isIntercomOpened, setIsIntercomOpened] = useState(false);
const { environment } = useEnvironment();
const location = useLocation();
const { getRedirectURL } = useRedirectURL();
const { inStudioRoute } = useRouteScopes();

/**
* TODO: this is a temporary work-around to let us work the different color palettes while testing locally.
* Eventually, we will want to only include 'LOCAL' in the conditional.
*/
const isLocalEnv = useMemo(
() =>
[EnvironmentEnum.DEVELOPMENT].includes(environment?.name as EnvironmentEnum) && isStudioRoute(location.pathname),
[environment, location]
() => [EnvironmentEnum.DEVELOPMENT].includes(environment?.name as EnvironmentEnum) && inStudioRoute,
[environment, inStudioRoute]
);

const isEqualToSampleEndpoint =
Expand All @@ -53,44 +51,53 @@ export function PrivatePageLayout() {
BRIDGE_ENDPOINTS_LEGACY_VERSIONS.includes(environment?.bridge?.url));
const showSampleModeBanner = isEqualToSampleEndpoint && location.pathname.includes('/workflows');

useEffect(() => {
const redirectURL = getRedirectURL();
if (redirectURL) {
// Note: Do not use react-router-dom. The version we have doesn't do instant cross origin redirects.
window.location.replace(redirectURL);

return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<EnsureOnboardingComplete>
<SpotLightProvider>
<IntercomProvider
appId={INTERCOM_APP_ID}
onShow={() => setIsIntercomOpened(true)}
onHide={() => setIsIntercomOpened(false)}
<SpotLightProvider>
<IntercomProvider
appId={INTERCOM_APP_ID}
onShow={() => setIsIntercomOpened(true)}
onHide={() => setIsIntercomOpened(false)}
>
<ErrorBoundary
fallback={({ error, eventId }) => (
<>
Sorry, but something went wrong. <br />
Our team has been notified and we are investigating.
<br />
<code>
<small style={{ color: 'lightGrey' }}>
Event Id: {eventId}.
<br />
{error.toString()}
</small>
</code>
</>
)}
>
<ErrorBoundary
fallback={({ error, eventId }) => (
<>
Sorry, but something went wrong. <br />
Our team has been notified and we are investigating.
<br />
<code>
<small style={{ color: 'lightGrey' }}>
Event Id: {eventId}.
<br />
{error.toString()}
</small>
</code>
</>
)}
>
<SpotLight>
<AppShell className={css({ '& *': { colorPalette: isLocalEnv ? 'mode.local' : 'mode.cloud' } })}>
<Sidebar />
<ContentShell>
{showSampleModeBanner && <SampleModeBanner />}
<FreeTrialBanner />
<HeaderNav />
<Outlet />
</ContentShell>
</AppShell>
</SpotLight>
</ErrorBoundary>
</IntercomProvider>
</SpotLightProvider>
</EnsureOnboardingComplete>
<SpotLight>
<AppShell className={css({ '& *': { colorPalette: isLocalEnv ? 'mode.local' : 'mode.cloud' } })}>
<Sidebar />
<ContentShell>
{showSampleModeBanner && <SampleModeBanner />}
<FreeTrialBanner />
<HeaderNav />
<Outlet />
</ContentShell>
</AppShell>
</SpotLight>
</ErrorBoundary>
</IntercomProvider>
</SpotLightProvider>
);
}
Loading

0 comments on commit 6d96ea5

Please sign in to comment.