Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -210,6 +220,7 @@ export interface FileRouteTypes {
| '/login'
| '/onboarding'
| '/sso-callback'
| '/onboarding/accept-invitation'
| '/onboarding/choose-organization'
| '/'
| '/orgs/$organization'
Expand All @@ -229,6 +240,7 @@ export interface FileRouteTypes {
| '/login'
| '/onboarding'
| '/sso-callback'
| '/onboarding/accept-invitation'
| '/onboarding/choose-organization'
| '/'
| '/ns/$namespace/connect'
Expand All @@ -247,6 +259,7 @@ export interface FileRouteTypes {
| '/sso-callback'
| '/_context/_cloud'
| '/_context/_engine'
| '/onboarding/accept-invitation'
| '/onboarding/choose-organization'
| '/_context/'
| '/_context/_cloud/orgs/$organization'
Expand Down Expand Up @@ -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: ''
Expand Down Expand Up @@ -529,10 +549,12 @@ const ContextRouteWithChildren =
ContextRoute._addFileChildren(ContextRouteChildren)

interface OnboardingRouteChildren {
OnboardingAcceptInvitationRoute: typeof OnboardingAcceptInvitationRoute
OnboardingChooseOrganizationRoute: typeof OnboardingChooseOrganizationRoute
}

const OnboardingRouteChildren: OnboardingRouteChildren = {
OnboardingAcceptInvitationRoute: OnboardingAcceptInvitationRoute,
OnboardingChooseOrganizationRoute: OnboardingChooseOrganizationRoute,
}

Expand Down
14 changes: 14 additions & 0 deletions frontend/src/routes/_context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand Down Expand Up @@ -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",
Expand Down
213 changes: 213 additions & 0 deletions frontend/src/routes/onboarding/accept-invitation.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex min-h-screen flex-col items-center justify-center bg-background py-4">
<div className="flex flex-col items-center gap-6">
<Logo className="h-10 mb-4" />
<OrgSignUpFlow ticket={search.__clerk_ticket} />
</div>
</div>
);
}

if (search.__clerk_status === "sign_in") {
// complete sign in flow
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background py-4">
<div className="flex flex-col items-center gap-6">
<Logo className="h-10 mb-4" />
<OrgSignInFlow ticket={search.__clerk_ticket} />
</div>
</div>
);
}

if (search.__clerk_status === "complete") {
// if we get here, the user is already signed in
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background py-4">
<div className="flex flex-col items-center gap-6">
<Logo className="h-10 mb-4" />
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Invitation Accepted</CardTitle>
<CardDescription>
You have successfully accepted the invitation.
You can now proceed to the dashboard.
</CardDescription>
</CardHeader>
<CardFooter>
<Button asChild>
<Link to="/">Go Home</Link>
</Button>
</CardFooter>
</Card>
</div>
</div>
);
}

return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background py-4">
<div className="flex flex-col items-center gap-6">
<Logo className="h-10 mb-4" />
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Invalid Invitation</CardTitle>
<CardDescription>
The invitation link is invalid. Please check the
link or contact support.
</CardDescription>
</CardHeader>
</Card>
</div>
</div>
);
}

function OrgSignUpFlow({ ticket }: { ticket: string }) {
const { signUp, setActive: setActiveSignUp } = useSignUp();
const navigate = useNavigate();

return (
<OrgSignUpForm.Form
defaultValues={{ password: "" }}
onSubmit={async ({ password }, form) => {
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.",
);
}
}
}}
>
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Welcome!</CardTitle>
<CardDescription>
Please set a password for your new account.
</CardDescription>
</CardHeader>
<CardContent>
<div>
<OrgSignUpForm.Password className="mb-4" />
</div>
</CardContent>
<CardFooter>
<OrgSignUpForm.Submit className="w-full">
Continue
</OrgSignUpForm.Submit>
</CardFooter>
</Card>
</OrgSignUpForm.Form>
);
}

function OrgSignInFlow({ ticket }: { ticket: string }) {
const { organization } = useOrganization();
const { signIn, setActive: setActiveSignIn } = useSignIn();
const isMounted = useIsMounted();

const [error, setError] = useState<string | null>(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 (
<Card className="w-full sm:w-96">
<CardHeader>
<CardTitle>Welcome back!</CardTitle>
<CardDescription>
You are signing in to {organization?.name || "your account"}
.
</CardDescription>
</CardHeader>
{error && (
<CardContent>
<div className="text-destructive">{error}</div>
</CardContent>
)}
</Card>
);
}
Loading