Skip to content

Commit

Permalink
Merge branch 'next' into v2-layouts-cont
Browse files Browse the repository at this point in the history
  • Loading branch information
antonjoel82 authored Jun 27, 2024
2 parents 4eca3a1 + 85507a8 commit 9b61b69
Show file tree
Hide file tree
Showing 33 changed files with 389 additions and 225 deletions.
1 change: 0 additions & 1 deletion apps/web/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "no-referrer"
X-Content-Type-Options = "nosniff"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"handlebars": "^4.7.7",
"highlight.js": "11.9.0",
"html-webpack-plugin": "5.5.3",
"js-cookie": "^3.0.5",
"jwt-decode": "^3.1.2",
"launchdarkly-react-client-sdk": "^3.3.2",
"less": "^4.1.0",
Expand Down Expand Up @@ -156,6 +157,7 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/js-cookie": "^3.0.6",
"eslint-plugin-storybook": "^0.6.13",
"http-server": "^0.13.0",
"less-loader": "4.1.0",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { useSegment } from './components/providers/SegmentProvider';
import * as mixpanel from 'mixpanel-browser';
import { useEffect } from 'react';
import { GetStartedPageV2 } from './studio/components/GetStartedPageV2';
import { novuOnboardedCookie } from './utils/cookies';

export const AppRoutes = () => {
const isImprovedOnboardingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_ONBOARDING_ENABLED);
Expand Down Expand Up @@ -171,7 +172,10 @@ export const AppRoutes = () => {
<Route path={ROUTES.LOCAL_STUDIO_AUTH} element={<LocalStudioAuthenticator />} />

<Route path={ROUTES.STUDIO} element={<StudioPageLayout />}>
<Route path="" element={<Navigate to={ROUTES.STUDIO_FLOWS} replace />} />
<Route
path=""
element={<Navigate to={novuOnboardedCookie.get() ? ROUTES.STUDIO_FLOWS : ROUTES.STUDIO_ONBOARDING} replace />}
/>
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowsListPage />} />
<Route path={ROUTES.STUDIO_FLOWS_VIEW} element={<WorkflowsDetailPage />} />
<Route path={ROUTES.STUDIO_FLOWS_STEP_EDITOR} element={<WorkflowsStepEditorPage />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../../hooks/useAuth';
import { ROUTES } from '../../../constants/routes';
import { useBlueprint } from '../../../hooks/index';
import { useBlueprint, useRedirectURL } from '../../../hooks';

export function EnsureOnboardingComplete({ children }: any) {
useBlueprint();
const location = useLocation();
const { getRedirectURL } = useRedirectURL();
const { currentOrganization, environmentId } = useAuth();

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

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 null;
}

return children;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { css } from '@novu/novui/css';
import { Button, Input, Title, Text } from '@novu/novui';
import { IconOutlineMenuBook } from '@novu/novui/icons';
import { HStack, Box } from '@novu/novui/jsx';
import { FC, useMemo, useState } from 'react';
import { FC, useState } from 'react';
import { validateBridgeUrl } from '../../../../api/bridge';
import { updateBridgeUrl } from '../../../../api/environment';
import { useEnvironment } from '../../../../hooks/useEnvironment';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const SyncInfoModal: FC<SyncInfoModalProps> = ({ isOpen, toggleOpen }) =>
},
{
value: 'other',
label: 'Other CI',
label: 'CLI',
content: (
<Prism withLineNumbers language="bash">
{getOtherCodeContent({ apiKey, bridgeUrl })}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from './useNotificationGroup';
export * from './useNovu';
export * from './useProcessVariables';
export * from './usePrompt';
export * from './useRedirectURL';
export * from './useSubscribers';
export * from './useTemplates';
export * from './useThemeChange';
Expand Down
35 changes: 29 additions & 6 deletions apps/web/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export function getTokenClaims(): IJwtClaims | null {
return token ? jwtDecode<IJwtClaims>(token) : null;
}

function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

export function useAuth() {
const ldClient = useLDClient();
const segment = useSegment();
Expand All @@ -57,7 +65,7 @@ export function useAuth() {
const hasToken = !!getToken();

useEffect(() => {
if (!getToken() && inPrivateRoute) {
if (!getToken() && inPrivateRoute && !inIframe()) {
navigate(ROUTES.AUTH_LOGIN, { state: { redirectTo: location } });
}
}, [navigate, inPrivateRoute, location]);
Expand Down Expand Up @@ -107,9 +115,25 @@ export function useAuth() {
navigate(ROUTES.AUTH_LOGIN);
}, [navigate, queryClient, segment]);

const redirectTo = useCallback(({ url, redirectURL }: { url: string; redirectURL?: string }) => {
const finalURL = new URL(url, window.location.origin);

if (redirectURL) {
finalURL.searchParams.append('redirect_url', redirectURL);
}

// Note: Do not use react-router-dom. The version we have doesn't do instant cross origin redirects.
window.location.replace(finalURL.href);
}, []);

const redirectToLogin = useCallback(
(redirectUrl?: string) => navigate(`${ROUTES.AUTH_LOGIN}?redirect_url=${redirectUrl}`),
[navigate]
({ redirectURL }: { redirectURL?: string } = {}) => redirectTo({ url: ROUTES.AUTH_LOGIN, redirectURL }),
[redirectTo]
);

const redirectToSignUp = useCallback(
({ redirectURL }: { redirectURL?: string } = {}) => redirectTo({ url: ROUTES.AUTH_SIGNUP, redirectURL }),
[redirectTo]
);

const { organizationId, environmentId } = getTokenClaims() || {};
Expand Down Expand Up @@ -161,9 +185,7 @@ export function useAuth() {
return {
inPublicRoute,
inPrivateRoute,
isUserLoading,
isOrganizationLoading,
isLoading: inPrivateRoute && (isUserLoading || isOrganizationLoading),
isLoading: hasToken && (isUserLoading || isOrganizationLoading),
currentUser: user,
organizations,
currentOrganization,
Expand All @@ -172,5 +194,6 @@ export function useAuth() {
environmentId,
organizationId,
redirectToLogin,
redirectToSignUp,
};
}
48 changes: 48 additions & 0 deletions apps/web/src/hooks/useRedirectURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import { createCookieHandler } from '../utils/cookies';
import { assertProtocol } from '../utils/url';

const novuRedirectURLCookie = createCookieHandler('nv_redirect_url');

const REDIRECT_URL_SEARCH_PARAM = 'redirect_url';

const REDIRECT_COOKIE_EXPIRY_DAYS = 7;

export function useRedirectURL() {
const setRedirectURL = useCallback(() => {
const redirectURLFromParams = new URL(window.location.href).searchParams.get(REDIRECT_URL_SEARCH_PARAM) || '';

// If there is a redirect URL in the URL, set it in the cookie.
if (redirectURLFromParams) {
// Protect against XSS attacks via the javascript: pseudo protocol.
assertProtocol(redirectURLFromParams);
// Expires in 7 days.
novuRedirectURLCookie.set(redirectURLFromParams, { expires: REDIRECT_COOKIE_EXPIRY_DAYS });

// Clean the URL so that the redirect URL doesn't get used again.
const url = new URL(window.location.href);
url.searchParams.delete(REDIRECT_URL_SEARCH_PARAM);
history.replaceState({}, '', url.href);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const getRedirectURL = useCallback(() => {
const redirectURLFromCookie = novuRedirectURLCookie.get();

// If there is a cookie in the URL, redirect to that URL. Otherwise, its a noop.
if (redirectURLFromCookie) {
// Clean the cookie first, so that it doesn't get used again.
novuRedirectURLCookie.remove();

return redirectURLFromCookie;
}

return '';
}, []);

return {
setRedirectURL,
getRedirectURL,
};
}
10 changes: 6 additions & 4 deletions apps/web/src/pages/auth/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { useForm } from 'react-hook-form';
import * as Sentry from '@sentry/react';
import { Center } from '@mantine/core';
import { PasswordInput, Button, colors, Input, Text } from '@novu/design-system';
import { useAuth } from '../../../hooks/useAuth';
import type { IResponseError } from '@novu/shared';
import { useVercelIntegration, useVercelParams } from '../../../hooks';
import { useAuth, useRedirectURL, useVercelIntegration, useVercelParams } from '../../../hooks';
import { useSegment } from '../../../components/providers/SegmentProvider';
import { api } from '../../../api/api.client';
import { useAcceptInvite } from './useAcceptInvite';
Expand All @@ -28,6 +27,11 @@ export interface LocationState {

export function LoginForm({ email, invitationToken }: LoginFormProps) {
const segment = useSegment();

const { setRedirectURL } = useRedirectURL();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setRedirectURL(), []);

const { login, currentUser, organizations } = useAuth();
const { startVercelSetup } = useVercelIntegration();
const { isFromVercel, params: vercelParams } = useVercelParams();
Expand All @@ -38,8 +42,6 @@ export function LoginForm({ email, invitationToken }: LoginFormProps) {
// TODO: Deprecate the legacy cameCased format in search param
const invitationTokenFromGithub = params.get('invitationToken') || params.get('invitation_token') || '';
const isRedirectedFromLoginPage = params.get('isLoginPage') || params.get('is_login_page') || '';
// TODO: Use redirectUrl if available and redirect post login to the URL. This should be used during the Local studio authentication flow
const redirectUrl = params.get('redirect_url') || '';

const { isLoading: isLoadingAcceptInvite, acceptInvite } = useAcceptInvite();
const navigate = useNavigate();
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/pages/auth/components/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
Expand All @@ -9,7 +9,7 @@ import { PasswordInput, Button, colors, Input, Text, Checkbox } from '@novu/desi

import { useAuth } from '../../../hooks/useAuth';
import { api } from '../../../api/api.client';
import { useVercelParams } from '../../../hooks';
import { useRedirectURL, useVercelParams } from '../../../hooks';
import { useAcceptInvite } from './useAcceptInvite';
import { PasswordRequirementPopover } from './PasswordRequirementPopover';
import { ROUTES } from '../../../constants/routes';
Expand All @@ -28,6 +28,9 @@ export type SignUpFormInputType = {

export function SignUpForm({ invitationToken, email }: SignUpFormProps) {
const navigate = useNavigate();
const { setRedirectURL } = useRedirectURL();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setRedirectURL(), []);

const { login } = useAuth();
const { isLoading: isAcceptInviteLoading, acceptInvite } = useAcceptInvite();
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/pages/studio-onboarding/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export const Header = ({ activeStepIndex = 0 }: { activeStepIndex?: number }) =>
active={activeStepIndex}
>
<Stepper.Step label="Add the endpoint"></Stepper.Step>
<Stepper.Step label="Create workflow"></Stepper.Step>
<Stepper.Step label="Test workflow"></Stepper.Step>
<Stepper.Step label="Test the workflow"></Stepper.Step>
<Stepper.Step label="Check your Inbox"></Stepper.Step>
</Stepper>
</div>
</VStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export const SetupTimeline = ({ testResponse }: { testResponse: { isLoading: boo
<MantineTimeline.Item
bullet={<CheckStatusIcon />}
lineVariant="dashed"
title="Connect to the endpoint"
title="Connect to the Novu Bridge Endpoint"
active={active >= 3}
>
<Text variant="main" color="typography.text.secondary">
{active < 3 ? 'Waiting for you to start the application' : 'Succefully connected to the Novu Endpoint'}
{active < 3 ? 'Waiting for you to start the application' : 'Succefully connected to the Novu Bridge Endpoint'}
</Text>
</MantineTimeline.Item>
</Timeline>
Expand Down
11 changes: 8 additions & 3 deletions apps/web/src/pages/studio-onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ import { ROUTES } from '../../constants/routes';
import { useNavigate } from 'react-router-dom';
import { useHealthCheck } from '../../studio/hooks/useBridgeAPI';
import { BridgeStatus } from '../../bridgeApi/bridgeApi.client';
import { useStudioState } from '../../studio/StudioStateProvider';
import { capitalizeFirstLetter } from '../../utils/string';

export const StudioOnboarding = () => {
const segment = useSegment();
const navigate = useNavigate();
const { testUser } = useStudioState();
const { data, isLoading } = useHealthCheck();

useEffect(() => {
segment.track('Add endpoint step started - [Onboarding - Signup]');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const welcomeMessage = `Welcome ${capitalizeFirstLetter(testUser?.firstName || '')}`.trim() + `. Let's get started!`;

return (
<Wrapper>
<Header />
Expand All @@ -31,7 +36,7 @@ export const StudioOnboarding = () => {
width: 'onboarding',
})}
>
<Title variant="page">Create an Novu endpoint</Title>
<Title variant="page">{welcomeMessage}</Title>
<Text
variant="main"
color="typography.text.secondary"
Expand All @@ -40,8 +45,8 @@ export const StudioOnboarding = () => {
marginTop: '50',
})}
>
To start sending your first workflows, you first need to connect Novu to your Bridge Endpoint. This setup
will create a sample Next.js project and pre-configured the @novu/framework client for you.
Send your first email notification, by connecting to your Novu Bridge Endpoint. This setup will create a
sample Next.js project with a pre-configured <code>@novu/framework</code>.
</Text>
<SetupTimeline testResponse={{ data: data as BridgeStatus, isLoading }} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/studio-onboarding/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const StudioOnboardingPreview = () => {
navigate({
pathname: ROUTES.STUDIO_ONBOARDING_SUCCESS,
search: createSearchParams({
transactionId: response.transactionId,
transactionId: response.data.transactionId,
}).toString(),
});
};
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/pages/studio-onboarding/success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { ROUTES } from '../../constants/routes';
import { Footer } from './components/Footer';
import { Header } from './components/Header';
import { Wrapper } from './components/Wrapper';
import { novuOnboardedCookie } from '../../utils/cookies';

const ONBOARDING_COOKIE_EXPIRY_DAYS = 10 * 365;

export const StudioOnboardingSuccess = () => {
const [searchParams] = useSearchParams();
Expand Down Expand Up @@ -65,6 +68,14 @@ export const StudioOnboardingSuccess = () => {

useEffect(() => {
segment.track('Test workflow step completed - [Onboarding - Signup]');

// Never expires! Well it does, in 10 years but you will change device or browser by then :)
novuOnboardedCookie.set('1', {
expires: ONBOARDING_COOKIE_EXPIRY_DAYS,
sameSite: 'none',
secure: window.location.protocol === 'https',
});

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

Expand Down
Loading

0 comments on commit 9b61b69

Please sign in to comment.