diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 68e2b187b9..70ac67d4b7 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as JoinRouteImport } from './routes/join' import { Route as ContextRouteImport } from './routes/_context' import { Route as ContextIndexRouteImport } from './routes/_context/index' import { Route as OnboardingChooseOrganizationRouteImport } from './routes/onboarding/choose-organization' +import { Route as OnboardingAcceptInvitationRouteImport } from './routes/onboarding/accept-invitation' import { Route as ContextEngineRouteImport } from './routes/_context/_engine' import { Route as ContextCloudRouteImport } from './routes/_context/_cloud' import { Route as ContextEngineNsNamespaceRouteImport } from './routes/_context/_engine/ns.$namespace' @@ -65,6 +66,12 @@ const OnboardingChooseOrganizationRoute = path: '/choose-organization', getParentRoute: () => OnboardingRoute, } as any) +const OnboardingAcceptInvitationRoute = + OnboardingAcceptInvitationRouteImport.update({ + id: '/accept-invitation', + path: '/accept-invitation', + getParentRoute: () => OnboardingRoute, + } as any) const ContextEngineRoute = ContextEngineRouteImport.update({ id: '/_engine', getParentRoute: () => ContextRoute, @@ -151,6 +158,7 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/onboarding': typeof OnboardingRouteWithChildren '/sso-callback': typeof SsoCallbackRoute + '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/': typeof ContextIndexRoute '/orgs/$organization': typeof ContextCloudOrgsOrganizationRouteWithChildren @@ -170,6 +178,7 @@ export interface FileRoutesByTo { '/login': typeof LoginRoute '/onboarding': typeof OnboardingRouteWithChildren '/sso-callback': typeof SsoCallbackRoute + '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/': typeof ContextIndexRoute '/ns/$namespace/connect': typeof ContextEngineNsNamespaceConnectRoute @@ -189,6 +198,7 @@ export interface FileRoutesById { '/sso-callback': typeof SsoCallbackRoute '/_context/_cloud': typeof ContextCloudRouteWithChildren '/_context/_engine': typeof ContextEngineRouteWithChildren + '/onboarding/accept-invitation': typeof OnboardingAcceptInvitationRoute '/onboarding/choose-organization': typeof OnboardingChooseOrganizationRoute '/_context/': typeof ContextIndexRoute '/_context/_cloud/orgs/$organization': typeof ContextCloudOrgsOrganizationRouteWithChildren @@ -210,6 +220,7 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/sso-callback' + | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/' | '/orgs/$organization' @@ -229,6 +240,7 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/sso-callback' + | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/' | '/ns/$namespace/connect' @@ -247,6 +259,7 @@ export interface FileRouteTypes { | '/sso-callback' | '/_context/_cloud' | '/_context/_engine' + | '/onboarding/accept-invitation' | '/onboarding/choose-organization' | '/_context/' | '/_context/_cloud/orgs/$organization' @@ -321,6 +334,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingChooseOrganizationRouteImport parentRoute: typeof OnboardingRoute } + '/onboarding/accept-invitation': { + id: '/onboarding/accept-invitation' + path: '/accept-invitation' + fullPath: '/onboarding/accept-invitation' + preLoaderRoute: typeof OnboardingAcceptInvitationRouteImport + parentRoute: typeof OnboardingRoute + } '/_context/_engine': { id: '/_context/_engine' path: '' @@ -529,10 +549,12 @@ const ContextRouteWithChildren = ContextRoute._addFileChildren(ContextRouteChildren) interface OnboardingRouteChildren { + OnboardingAcceptInvitationRoute: typeof OnboardingAcceptInvitationRoute OnboardingChooseOrganizationRoute: typeof OnboardingChooseOrganizationRoute } const OnboardingRouteChildren: OnboardingRouteChildren = { + OnboardingAcceptInvitationRoute: OnboardingAcceptInvitationRoute, OnboardingChooseOrganizationRoute: OnboardingChooseOrganizationRoute, } diff --git a/frontend/src/routes/_context.tsx b/frontend/src/routes/_context.tsx index 5d9411cd75..81e2ebd4e1 100644 --- a/frontend/src/routes/_context.tsx +++ b/frontend/src/routes/_context.tsx @@ -33,6 +33,9 @@ const searchSchema = z n: z.array(z.string()).optional(), u: z.string().optional(), t: z.string().optional(), + // clerk related + __clerk_ticket: z.string().optional(), + __clerk_status: z.string().optional(), }) .and(z.record(z.string(), z.any())); @@ -67,6 +70,17 @@ export const Route = createFileRoute("/_context")({ return await match(route.context) .with({ __type: "cloud" }, () => async () => { await waitForClerk(route.context.clerk); + + if ( + route.location.search.__clerk_ticket && + route.location.search.__clerk_status + ) { + throw redirect({ + to: "/onboarding/accept-invitation", + search: { ...route.location.search }, + }); + } + if (!route.context.clerk.user) { throw redirect({ to: "/login", diff --git a/frontend/src/routes/onboarding/accept-invitation.tsx b/frontend/src/routes/onboarding/accept-invitation.tsx new file mode 100644 index 0000000000..bf65d227ca --- /dev/null +++ b/frontend/src/routes/onboarding/accept-invitation.tsx @@ -0,0 +1,213 @@ +import { isClerkAPIResponseError } from "@clerk/clerk-js"; +import { useOrganization, useSignIn, useSignUp } from "@clerk/clerk-react"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { useIsMounted } from "usehooks-ts"; +import * as OrgSignUpForm from "@/app/forms/org-sign-up-form"; +import { Logo } from "@/app/logo"; +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + toast, +} from "@/components"; + +export const Route = createFileRoute("/onboarding/accept-invitation")({ + component: RouteComponent, +}); + +function RouteComponent() { + const search = Route.useSearch(); + + if (search.__clerk_status === "sign_up") { + // display sign up flow + return ( +
+
+ + +
+
+ ); + } + + if (search.__clerk_status === "sign_in") { + // complete sign in flow + return ( +
+
+ + +
+
+ ); + } + + if (search.__clerk_status === "complete") { + // if we get here, the user is already signed in + return ( +
+
+ + + + Invitation Accepted + + You have successfully accepted the invitation. + You can now proceed to the dashboard. + + + + + + +
+
+ ); + } + + return ( +
+
+ + + + Invalid Invitation + + The invitation link is invalid. Please check the + link or contact support. + + + +
+
+ ); +} + +function OrgSignUpFlow({ ticket }: { ticket: string }) { + const { signUp, setActive: setActiveSignUp } = useSignUp(); + const navigate = useNavigate(); + + return ( + { + try { + const signUpAttempt = await signUp?.create({ + strategy: "ticket", + ticket, + password, + }); + + if (signUpAttempt?.status === "complete") { + await setActiveSignUp?.({ + session: signUpAttempt.createdSessionId, + }); + await navigate({ to: "/" }); + } else { + console.error( + "Sign up attempt not complete", + signUpAttempt, + ); + toast.error( + "An error occurred during sign up. Please try again.", + ); + } + } catch (e) { + if (isClerkAPIResponseError(e)) { + for (const error of e.errors) { + form.setError( + (error.meta?.paramName || "root") as "root", + { + message: error.longMessage, + }, + ); + } + } else { + toast.error( + "An unknown error occurred. Please try again.", + ); + } + } + }} + > + + + Welcome! + + Please set a password for your new account. + + + +
+ +
+
+ + + Continue + + +
+
+ ); +} + +function OrgSignInFlow({ ticket }: { ticket: string }) { + const { organization } = useOrganization(); + const { signIn, setActive: setActiveSignIn } = useSignIn(); + const isMounted = useIsMounted(); + + const [error, setError] = useState(null); + + useEffect(() => { + async function signInWithTicket() { + const signInAttempt = await signIn?.create({ + strategy: "ticket", + ticket, + }); + + // If the sign-in was successful, set the session to active + if (signInAttempt?.status === "complete") { + await setActiveSignIn?.({ + session: signInAttempt?.createdSessionId, + }); + } else { + // If the sign-in attempt is not complete, check why. + // User may need to complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)); + } + } + + signInWithTicket().catch((e) => { + if (isClerkAPIResponseError(e)) { + setError(e.message); + } else { + setError("An unknown error occurred. Please try again."); + } + }); + }, [isMounted]); + + return ( + + + Welcome back! + + You are signing in to {organization?.name || "your account"} + . + + + {error && ( + +
{error}
+
+ )} +
+ ); +}