From c158f3da7541c32371d86959500ab79c89d879ec Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:02:05 +0200 Subject: [PATCH 1/4] feat: Detect and upgrade users with lissing metadata --- .vscode/settings.json | 1 + apps/nextjs/scripts/local-dev.mjs | 7 +- apps/nextjs/src/app/aila/[id]/share/page.tsx | 7 +- apps/nextjs/src/app/onboarding/onboarding.tsx | 144 ++++-------------- apps/nextjs/src/app/onboarding/page.tsx | 2 +- .../components/Onboarding/AcceptTermsForm.tsx | 138 +++++++++++++++++ .../Onboarding/LegacyUpgradeNotice.tsx | 28 ++++ apps/nextjs/src/hooks/useClerkDemoMetadata.ts | 33 +++- apps/nextjs/src/hooks/useReloadSession.ts | 11 ++ .../nextjs/src/middlewares/auth.middleware.ts | 102 +++++++++++-- .../src/mocks/clerk/nextjsComponents.tsx | 2 +- packages/api/src/router/auth.ts | 58 ++++--- packages/core/src/models/demoUsers.ts | 26 +++- 13 files changed, 401 insertions(+), 158 deletions(-) create mode 100644 apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx create mode 100644 apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx create mode 100644 apps/nextjs/src/hooks/useReloadSession.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a0dc78ce6..159b246c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -93,6 +93,7 @@ "posttest", "pptxgen", "pptxgenjs", + "Preloadable", "PSED", "PSHE", "psql", diff --git a/apps/nextjs/scripts/local-dev.mjs b/apps/nextjs/scripts/local-dev.mjs index 270261082..fee10aa64 100644 --- a/apps/nextjs/scripts/local-dev.mjs +++ b/apps/nextjs/scripts/local-dev.mjs @@ -11,6 +11,10 @@ const routesToPreBuild = [ "/aila", ]; +const headers = { + "x-dev-preload": "true", +}; + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const preBuildRoutes = async ( @@ -29,7 +33,7 @@ const preBuildRoutes = async ( return limit(() => { if (typeof route === "string") { return axios - .get(`http://localhost:2525${route}`) + .get(`http://localhost:2525${route}`, { headers }) .then(() => console.log(`Pre-built route: ${route}`)) .catch((error) => { console.log(`Error pre-building route: ${route}`, error.message); @@ -39,6 +43,7 @@ const preBuildRoutes = async ( return axios({ method: route.method, url: `http://localhost:2525${route.url}`, + headers, }) .then(() => console.log( diff --git a/apps/nextjs/src/app/aila/[id]/share/page.tsx b/apps/nextjs/src/app/aila/[id]/share/page.tsx index 6c959531d..be64ec031 100644 --- a/apps/nextjs/src/app/aila/[id]/share/page.tsx +++ b/apps/nextjs/src/app/aila/[id]/share/page.tsx @@ -1,5 +1,6 @@ import { User, clerkClient } from "@clerk/nextjs/server"; import { getSessionModerations } from "@oakai/aila/src/features/moderation/getSessionModerations"; +import { demoUsers } from "@oakai/core"; import { isToxic } from "@oakai/core/src/utils/ailaModeration/helpers"; import { PersistedModerationBase } from "@oakai/core/src/utils/ailaModeration/moderationSchema"; import { type Metadata } from "next"; @@ -25,8 +26,10 @@ export async function generateMetadata({ } function userCanShare(user: User) { - const isDemoUser = Boolean(user.publicMetadata.isDemoUser ?? "true"); - + if (!demoUsers.isDemoStatusSet(user)) { + return false; + } + const isDemoUser = Boolean(user.publicMetadata.labs.isDemoUser ?? "true"); if (!isDemoUser) { return true; } diff --git a/apps/nextjs/src/app/onboarding/onboarding.tsx b/apps/nextjs/src/app/onboarding/onboarding.tsx index 6293d8a5b..22866893b 100644 --- a/apps/nextjs/src/app/onboarding/onboarding.tsx +++ b/apps/nextjs/src/app/onboarding/onboarding.tsx @@ -1,128 +1,46 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef } from "react"; +import { useUser } from "@clerk/nextjs"; import logger from "@oakai/logger/browser"; -import { - OakBox, - OakFlex, - OakHeading, - OakLink, - OakP, - OakPrimaryButton, - OakSpan, -} from "@oaknational/oak-components"; -import Link from "next/link"; +import { useReloadSession } from "hooks/useReloadSession"; -import Button from "@/components/Button"; -import CheckBox from "@/components/CheckBox"; -import SignUpSignInLayout from "@/components/SignUpSignInLayout"; -import TermsContent from "@/components/TermsContent"; +import { AcceptTermsForm } from "@/components/Onboarding/AcceptTermsForm"; +import { LegacyUpgradeNotice } from "@/components/Onboarding/LegacyUpgradeNotice"; import { trpc } from "@/utils/trpc"; export const OnBoarding = () => { - const [dropDownOpen, setDropDownOpen] = useState(true); - const [termsAcceptedLocal, setTermsAcceptedLocal] = useState(false); - const [privacyAcceptedLocal, setPrivacyAcceptedLocal] = useState(false); - const acceptTerms = trpc.auth.acceptTerms.useMutation(); - - const handleAcceptTermsOfUse = async () => { - try { - const response = await acceptTerms.mutateAsync({ - termsOfUse: new Date(), - privacyPolicy: privacyAcceptedLocal ? new Date() : false, - }); - - if (!response?.acceptedTermsOfUse) { - throw new Error("Could not accept terms of use"); + const { user } = useUser(); + const reloadSession = useReloadSession(); + const setDemoStatus = trpc.auth.setDemoStatus.useMutation(); + + const userHasAlreadyAcceptedTerms = + user?.publicMetadata?.["labs"]?.["isOnboarded"]; + + // Edge case: Legacy users have already accepted terms but don't have a demo status + const isHandlingLegacyCase = useRef(false); + useEffect(() => { + async function handleDemoStatusSet() { + if (userHasAlreadyAcceptedTerms && !isHandlingLegacyCase.current) { + isHandlingLegacyCase.current = true; + logger.debug("User has already accepted terms"); + await setDemoStatus.mutateAsync(); + logger.debug("Demo status set successfully"); + await reloadSession(); + logger.debug("Session token refreshed successfully. Redirecting"); + window.location.href = "/"; } - - logger.debug("Terms of use accepted successfully."); - window.location.href = "/"; - } catch (error) { - logger.error(error, "An error occurred while accepting terms of use"); } - }; - - return ( - - - - This product is experimental and uses AI - - - - We have worked to ensure that our tools are as high quality and as - safe as possible but we cannot guarantee accuracy. Please use with - caution. - - + handleDemoStatusSet(); + }, [userHasAlreadyAcceptedTerms, setDemoStatus, reloadSession]); - - - - Keep me updated with latest Oak AI experiments, resources and - other helpful content by email. You can unsubscribe at any time. - See our{" "} - - privacy policy - - . - - - + if (userHasAlreadyAcceptedTerms) { + return ; + } - {termsAcceptedLocal ? ( - -

- Terms accepted, if the page does not reload please refresh and - navigate to home. -

-
- ) : ( - - - { - handleAcceptTermsOfUse(); - setTermsAcceptedLocal(true); - }} - > - I understand - - - )} - {!dropDownOpen && ( - - - - )} -
-
- ); + // For the typical new user, show the accept terms form + return ; }; export default OnBoarding; diff --git a/apps/nextjs/src/app/onboarding/page.tsx b/apps/nextjs/src/app/onboarding/page.tsx index 5952d052a..a00144374 100644 --- a/apps/nextjs/src/app/onboarding/page.tsx +++ b/apps/nextjs/src/app/onboarding/page.tsx @@ -1,5 +1,5 @@ import OnBoarding from "./onboarding"; -export default function OnBoardingPage() { +export default async function OnBoardingPage() { return ; } diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx new file mode 100644 index 000000000..b255293de --- /dev/null +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useState } from "react"; + +import { useUser } from "@clerk/nextjs"; +import logger from "@oakai/logger/browser"; +import { + OakBox, + OakFlex, + OakHeading, + OakLink, + OakP, + OakPrimaryButton, + OakSpan, +} from "@oaknational/oak-components"; +import { useReloadSession } from "hooks/useReloadSession"; +import Link from "next/link"; + +import Button from "@/components/Button"; +import CheckBox from "@/components/CheckBox"; +import SignUpSignInLayout from "@/components/SignUpSignInLayout"; +import TermsContent from "@/components/TermsContent"; +import { trpc } from "@/utils/trpc"; + +export const AcceptTermsForm = ({}) => { + const [dropDownOpen, setDropDownOpen] = useState(true); + const { isLoaded } = useUser(); + const reloadSession = useReloadSession(); + + const [termsAcceptedLocal, setTermsAcceptedLocal] = useState(false); + const [privacyAcceptedLocal, setPrivacyAcceptedLocal] = useState(false); + const setDemoStatus = trpc.auth.setDemoStatus.useMutation(); + const acceptTerms = trpc.auth.acceptTerms.useMutation({}); + + const handleAcceptTermsOfUse = async () => { + try { + await setDemoStatus.mutateAsync(); + logger.debug("Demo status set successfully"); + + const response = await acceptTerms.mutateAsync({ + termsOfUse: new Date(), + privacyPolicy: privacyAcceptedLocal ? new Date() : false, + }); + + if (!response?.acceptedTermsOfUse) { + throw new Error("Could not accept terms of use"); + } + logger.debug("Terms of use accepted successfully."); + + await reloadSession(); + logger.debug("Session token refreshed successfully. Redirecting"); + + window.location.href = "/"; + } catch (error) { + logger.error(error, "An error occurred while accepting terms of use"); + } + }; + + return ( + + + + This product is experimental and uses AI + + + + We have worked to ensure that our tools are as high quality and as + safe as possible but we cannot guarantee accuracy. Please use with + caution. + + + + + + + Keep me updated with latest Oak AI experiments, resources and + other helpful content by email. You can unsubscribe at any time. + See our{" "} + + privacy policy + + . + + + + + {termsAcceptedLocal ? ( + +

+ Terms accepted, if the page does not reload please refresh and + navigate to home. +

+
+ ) : ( + + + { + handleAcceptTermsOfUse(); + setTermsAcceptedLocal(true); + }} + > + I understand + + + )} + {!dropDownOpen && ( + + + + )} +
+
+ ); +}; diff --git a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx new file mode 100644 index 000000000..bd641544a --- /dev/null +++ b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { OakBox, OakFlex, OakHeading } from "@oaknational/oak-components"; + +import LoadingWheel from "@/components/LoadingWheel"; +import SignUpSignInLayout from "@/components/SignUpSignInLayout"; + +export const LegacyUpgradeNotice = ({}) => { + return ( + + + + Preparing your account + + + + + + + ); +}; diff --git a/apps/nextjs/src/hooks/useClerkDemoMetadata.ts b/apps/nextjs/src/hooks/useClerkDemoMetadata.ts index ec32c2399..b309fbdf7 100644 --- a/apps/nextjs/src/hooks/useClerkDemoMetadata.ts +++ b/apps/nextjs/src/hooks/useClerkDemoMetadata.ts @@ -1,6 +1,9 @@ import { useMemo } from "react"; import { useUser } from "#clerk/nextjs"; +import { addBreadcrumb } from "@sentry/nextjs"; + +type User = ReturnType["user"]; type UseClerkDemoMetadataReturn = | { @@ -12,6 +15,28 @@ type UseClerkDemoMetadataReturn = userType: undefined; }; +type LabsUser = User & { + publicMetadata: { + labs?: { + isDemoUser?: boolean; + isOnboarded?: boolean; + }; + }; +}; + +type UserWithDemoStatus = User & { + publicMetadata: { + labs: { + isDemoUser: boolean; + }; + }; +}; + +export function isDemoStatusSet(user: LabsUser): user is UserWithDemoStatus { + const labsMetadata = user.publicMetadata.labs || {}; + return "isDemoUser" in labsMetadata; +} + function getResult( user: ReturnType, ): UseClerkDemoMetadataReturn { @@ -32,8 +57,8 @@ function getResult( } // User is logged in, but has not completed onboarding - if (!("isDemoUser" in user.user.publicMetadata)) { - console.warn("User demo status is unknown"); + if (!isDemoStatusSet(user.user)) { + addBreadcrumb({ message: "User demo status is unknown" }); return { isSet: false, userType: undefined, @@ -43,7 +68,9 @@ function getResult( // User is logged in and has completed onboarding return { isSet: true, - userType: Boolean(user.user.publicMetadata.isDemoUser) ? "Demo" : "Full", + userType: Boolean(user.user.publicMetadata.labs.isDemoUser) + ? "Demo" + : "Full", }; } diff --git a/apps/nextjs/src/hooks/useReloadSession.ts b/apps/nextjs/src/hooks/useReloadSession.ts new file mode 100644 index 000000000..ba049cba3 --- /dev/null +++ b/apps/nextjs/src/hooks/useReloadSession.ts @@ -0,0 +1,11 @@ +import { useCallback } from "react"; + +import { useSession } from "@clerk/nextjs"; + +export const useReloadSession = () => { + const { session } = useSession(); + + return useCallback(async () => { + await session?.getToken({ skipCache: true }); + }, [session]); +}; diff --git a/apps/nextjs/src/middlewares/auth.middleware.ts b/apps/nextjs/src/middlewares/auth.middleware.ts index 872a41c89..42168e5b6 100644 --- a/apps/nextjs/src/middlewares/auth.middleware.ts +++ b/apps/nextjs/src/middlewares/auth.middleware.ts @@ -7,6 +7,15 @@ import { NextFetchEvent, NextRequest, NextResponse } from "next/server"; import { sentrySetUser } from "@/lib/sentry/sentrySetUser"; +declare global { + interface CustomJwtSessionClaims { + labs: { + isDemoUser: boolean | null; + isOnboarded: boolean | null; + }; + } +} + const publicRoutes = [ "/api/health", "/aila/health", @@ -38,26 +47,90 @@ const publicRoutes = [ "/sign-up(.*)", ]; -if (process.env.NODE_ENV === "development") { - // This allows us to warm up the chat server with live reload - // So we get something closer to the live experience - // Without having to wait for each page to compile as we navigate - publicRoutes.push("/api/chat"); - publicRoutes.push("/aila"); -} - const isPublicRoute = createRouteMatcher(publicRoutes); +// This allows us to warm up the chat server with live reload +// So we get something closer to the live experience +// Without having to wait for each page to compile as we navigate +const isPreloadableRoute = createRouteMatcher([ + ...publicRoutes, + "/api/chat", + "/aila", +]); + +const onboardingUrl = "/onboarding"; +const isOnboardingRoute = createRouteMatcher([ + onboardingUrl, + "/sign-in", + // NOTE: Be careful that this request doesn't batch as it will change the path + "/api/trpc/main/auth.setDemoStatus", + "/api/trpc/main/auth.acceptTerms", + "/api/trpc/main", +]); + +const isHomepage = createRouteMatcher(["/"]); + +const shouldInterceptRouteForOnboarding = (req: NextRequest) => { + if (isOnboardingRoute(req)) { + return false; + } + if (isHomepage(req)) { + return true; + } + if (isPublicRoute(req)) { + return false; + } + return true; +}; + +const needsToCompleteOnboarding = (sessionClaims: CustomJwtSessionClaims) => { + const labs = sessionClaims.labs; + return !labs.isOnboarded || labs.isDemoUser === null; +}; + +const LOG = false; +const logger = (request: NextRequest) => (message: string) => { + if (LOG) { + console.log(`[AUTH] ${request.url} ${message}`); + } +}; + function conditionallyProtectRoute( auth: ClerkMiddlewareAuth, - request: NextRequest, + req: NextRequest, ) { const authObject = auth(); + const { userId, redirectToSignIn, sessionClaims } = authObject; + const log = logger(req); + sentrySetUser(authObject); - if (!isPublicRoute(request)) { - authObject.protect(); + if (userId && needsToCompleteOnboarding(sessionClaims)) { + if (shouldInterceptRouteForOnboarding(req)) { + log("Incomplete onboarding: REDIRECT"); + return NextResponse.redirect(new URL("/onboarding", req.url)); + } + } + + if (isPublicRoute(req)) { + log("Public route: ALLOW"); + return; } + + if (process.env.NODE_ENV === "development" && req.headers["x-dev-preload"]) { + if (isPreloadableRoute(req)) { + log("Dev preload route: ALLOW"); + return; + } + } + + if (!userId) { + log("Protected route: REDIRECT"); + return redirectToSignIn({ returnBackUrl: req.url }); + } + + log("Protected route: ALLOW"); + return; } export async function authMiddleware( @@ -67,10 +140,9 @@ export async function authMiddleware( const configuredClerkMiddleware = clerkMiddleware(conditionallyProtectRoute); const response = await configuredClerkMiddleware(request, event); - - if (!response) { - return NextResponse.next({ request }); + if (response) { + return response; } - return response; + return NextResponse.next({ request }); } diff --git a/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx b/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx index 739c6290a..3a8c8002f 100644 --- a/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx +++ b/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx @@ -5,7 +5,7 @@ const mockUser = { firstName: "John", lastName: "Doe", emailAddresses: [{ emailAddress: "john@example.com" }], - publicMetadata: { isDemoUser: true }, + publicMetadata: { labs: { isDemoUser: true } }, // Add other user properties as needed }; diff --git a/packages/api/src/router/auth.ts b/packages/api/src/router/auth.ts index a8725d1e1..24f6168cb 100644 --- a/packages/api/src/router/auth.ts +++ b/packages/api/src/router/auth.ts @@ -47,43 +47,61 @@ export const authRouter = router({ .mutation(async ({ ctx, input }) => { const { userId } = ctx.auth; if (typeof userId === "string") { - const user = await clerkClient.users.getUser(userId); - - const { region, isDemoRegion: isDemoUser } = - await demoUsers.getUserRegion( - user, - ctx.req.headers.get("cf-ipcountry"), - ); - await clerkClient.users.updateUserMetadata(userId, { publicMetadata: { - // legacy field for demo users. To remove after transition to labs namespace - isDemoUser, labs: { - isDemoUser, isOnboarded: !!input.termsOfUse, }, }, privateMetadata: { acceptedPrivacyPolicy: input.privacyPolicy, acceptedTermsOfUse: input.termsOfUse, - region, }, }); const updatedUser = await clerkClient.users.getUser(userId); - await posthogServerClient.identify({ - distinctId: userId, - properties: { - isDemoUser, - }, - }); - const { acceptedPrivacyPolicy, acceptedTermsOfUse } = updatedUser.privateMetadata; - return { acceptedPrivacyPolicy, acceptedTermsOfUse, isDemoUser }; + return { acceptedPrivacyPolicy, acceptedTermsOfUse }; } }), + + setDemoStatus: protectedProcedure.mutation(async ({ ctx }) => { + const { userId } = ctx.auth; + if (typeof userId === "string") { + const user = await clerkClient.users.getUser(userId); + + if (demoUsers.isDemoStatusSet(user)) { + return { isDemoUser: user.publicMetadata.labs }; + } + + const { region, isDemoRegion: isDemoUser } = + await demoUsers.getUserRegion( + user, + ctx.req.headers.get("cf-ipcountry"), + ); + + await clerkClient.users.updateUserMetadata(userId, { + publicMetadata: { + labs: { + isDemoUser, + }, + }, + privateMetadata: { + region, + }, + }); + + await posthogServerClient.identify({ + distinctId: userId, + properties: { + isDemoUser, + }, + }); + + return { isDemoUser }; + } + }), }); diff --git a/packages/core/src/models/demoUsers.ts b/packages/core/src/models/demoUsers.ts index ac8e54bf7..80304cd5a 100644 --- a/packages/core/src/models/demoUsers.ts +++ b/packages/core/src/models/demoUsers.ts @@ -10,6 +10,23 @@ if (process.env.NODE_ENV === "production" && DEVELOPMENT_USER_REGION) { const GEO_RESTRICTIONS_ENABLED = process.env.NEXT_PUBLIC_DEMO_ACCOUNTS_ENABLED === "true"; +type LabsUser = User & { + publicMetadata: { + labs?: { + isDemoUser?: boolean; + isOnboarded?: boolean; + }; + }; +}; + +type UserWithDemoStatus = User & { + publicMetadata: { + labs: { + isDemoUser: boolean; + }; + }; +}; + function isOakDemoUser(user: User) { return user.emailAddresses.some( (email) => @@ -44,14 +61,19 @@ class DemoUsers { return { region, isDemoRegion }; } + isDemoStatusSet(user: LabsUser): user is UserWithDemoStatus { + const labsMetadata = user.publicMetadata.labs || {}; + return "isDemoUser" in labsMetadata && labsMetadata.isDemoUser !== null; + } + isDemoUser(user: User): boolean { if (!GEO_RESTRICTIONS_ENABLED) { return false; } - if (!("isDemoUser" in user.publicMetadata)) { + if (!this.isDemoStatusSet(user)) { throw new Error("User metadata is missing isDemoUser field"); } - return Boolean(user.publicMetadata.isDemoUser); + return Boolean(user.publicMetadata.labs.isDemoUser); } } From 07fc896caca310c4b14b8c1451499e7acdf987d2 Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:06:10 +0200 Subject: [PATCH 2/4] fix: address sonarcloud warnings --- apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx | 2 +- apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx | 2 +- apps/nextjs/src/hooks/useClerkDemoMetadata.ts | 4 +--- apps/nextjs/src/middlewares/auth.middleware.ts | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx index b255293de..49ff3f22f 100644 --- a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx @@ -22,7 +22,7 @@ import SignUpSignInLayout from "@/components/SignUpSignInLayout"; import TermsContent from "@/components/TermsContent"; import { trpc } from "@/utils/trpc"; -export const AcceptTermsForm = ({}) => { +export const AcceptTermsForm = () => { const [dropDownOpen, setDropDownOpen] = useState(true); const { isLoaded } = useUser(); const reloadSession = useReloadSession(); diff --git a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx index bd641544a..2a6b41726 100644 --- a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx +++ b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.tsx @@ -5,7 +5,7 @@ import { OakBox, OakFlex, OakHeading } from "@oaknational/oak-components"; import LoadingWheel from "@/components/LoadingWheel"; import SignUpSignInLayout from "@/components/SignUpSignInLayout"; -export const LegacyUpgradeNotice = ({}) => { +export const LegacyUpgradeNotice = () => { return ( Date: Thu, 29 Aug 2024 11:50:42 +0200 Subject: [PATCH 3/4] Add reason to redirect --- apps/nextjs/src/app/onboarding/onboarding.tsx | 2 +- apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/nextjs/src/app/onboarding/onboarding.tsx b/apps/nextjs/src/app/onboarding/onboarding.tsx index 22866893b..afd213fe5 100644 --- a/apps/nextjs/src/app/onboarding/onboarding.tsx +++ b/apps/nextjs/src/app/onboarding/onboarding.tsx @@ -29,7 +29,7 @@ export const OnBoarding = () => { logger.debug("Demo status set successfully"); await reloadSession(); logger.debug("Session token refreshed successfully. Redirecting"); - window.location.href = "/"; + window.location.href = "/?reason=metadata-upgraded"; } } handleDemoStatusSet(); diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx index 49ff3f22f..0b27c6bae 100644 --- a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx @@ -50,7 +50,7 @@ export const AcceptTermsForm = () => { await reloadSession(); logger.debug("Session token refreshed successfully. Redirecting"); - window.location.href = "/"; + window.location.href = "/?reason=onboarded"; } catch (error) { logger.error(error, "An error occurred while accepting terms of use"); } From 763d6ac6c83154f88f5384f92a36f2e1acdd9c7b Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:03:21 +0200 Subject: [PATCH 4/4] Tweaks from PRm review --- apps/nextjs/src/app/onboarding/page.tsx | 2 +- .../components/Onboarding/AcceptTermsForm.tsx | 7 +-- .../nextjs/src/middlewares/auth.middleware.ts | 3 +- packages/api/src/router/auth.ts | 51 +++++++++---------- packages/core/src/models/demoUsers.ts | 2 +- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/nextjs/src/app/onboarding/page.tsx b/apps/nextjs/src/app/onboarding/page.tsx index a00144374..5952d052a 100644 --- a/apps/nextjs/src/app/onboarding/page.tsx +++ b/apps/nextjs/src/app/onboarding/page.tsx @@ -1,5 +1,5 @@ import OnBoarding from "./onboarding"; -export default async function OnBoardingPage() { +export default function OnBoardingPage() { return ; } diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx index 0b27c6bae..54ce49d93 100644 --- a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx @@ -37,9 +37,10 @@ export const AcceptTermsForm = () => { await setDemoStatus.mutateAsync(); logger.debug("Demo status set successfully"); + const now = new Date(); const response = await acceptTerms.mutateAsync({ - termsOfUse: new Date(), - privacyPolicy: privacyAcceptedLocal ? new Date() : false, + termsOfUse: now, + privacyPolicy: privacyAcceptedLocal ? now : false, }); if (!response?.acceptedTermsOfUse) { @@ -97,7 +98,7 @@ export const AcceptTermsForm = () => { {termsAcceptedLocal ? (

- Terms accepted, if the page does not reload please refresh and + Terms accepted. If the page does not reload please refresh and navigate to home.

diff --git a/apps/nextjs/src/middlewares/auth.middleware.ts b/apps/nextjs/src/middlewares/auth.middleware.ts index b104c070b..16b41e676 100644 --- a/apps/nextjs/src/middlewares/auth.middleware.ts +++ b/apps/nextjs/src/middlewares/auth.middleware.ts @@ -58,9 +58,8 @@ const isPreloadableRoute = createRouteMatcher([ "/aila", ]); -const onboardingUrl = "/onboarding"; const isOnboardingRoute = createRouteMatcher([ - onboardingUrl, + "/onboarding", "/sign-in", // NOTE: Be careful that this request doesn't batch as it will change the path "/api/trpc/main/auth.setDemoStatus", diff --git a/packages/api/src/router/auth.ts b/packages/api/src/router/auth.ts index 24f6168cb..63151afe7 100644 --- a/packages/api/src/router/auth.ts +++ b/packages/api/src/router/auth.ts @@ -70,38 +70,35 @@ export const authRouter = router({ setDemoStatus: protectedProcedure.mutation(async ({ ctx }) => { const { userId } = ctx.auth; - if (typeof userId === "string") { - const user = await clerkClient.users.getUser(userId); + const user = await clerkClient.users.getUser(userId); - if (demoUsers.isDemoStatusSet(user)) { - return { isDemoUser: user.publicMetadata.labs }; - } + if (demoUsers.isDemoStatusSet(user)) { + return { isDemoUser: user.publicMetadata.labs }; + } - const { region, isDemoRegion: isDemoUser } = - await demoUsers.getUserRegion( - user, - ctx.req.headers.get("cf-ipcountry"), - ); + const { region, isDemoRegion: isDemoUser } = await demoUsers.getUserRegion( + user, + ctx.req.headers.get("cf-ipcountry"), + ); - await clerkClient.users.updateUserMetadata(userId, { - publicMetadata: { - labs: { - isDemoUser, - }, - }, - privateMetadata: { - region, - }, - }); - - await posthogServerClient.identify({ - distinctId: userId, - properties: { + await clerkClient.users.updateUserMetadata(userId, { + publicMetadata: { + labs: { isDemoUser, }, - }); + }, + privateMetadata: { + region, + }, + }); - return { isDemoUser }; - } + posthogServerClient.identify({ + distinctId: userId, + properties: { + isDemoUser, + }, + }); + + return { isDemoUser }; }), }); diff --git a/packages/core/src/models/demoUsers.ts b/packages/core/src/models/demoUsers.ts index 80304cd5a..e9a6c668e 100644 --- a/packages/core/src/models/demoUsers.ts +++ b/packages/core/src/models/demoUsers.ts @@ -19,7 +19,7 @@ type LabsUser = User & { }; }; -type UserWithDemoStatus = User & { +type UserWithDemoStatus = LabsUser & { publicMetadata: { labs: { isDemoUser: boolean;