feat: switch to workspace on invite success

This commit is contained in:
Henry
2025-09-26 21:38:05 +01:00
parent bd97362d90
commit f22a0332ab
2 changed files with 95 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useRouter } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@@ -46,6 +46,8 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
); );
const [hasLoaded, setHasLoaded] = useState(false); const [hasLoaded, setHasLoaded] = useState(false);
const workspacePublicId = useSearchParams().get("workspacePublicId");
const { data, isLoading } = api.workspace.all.useQuery(); const { data, isLoading } = api.workspace.all.useQuery();
const utils = api.useUtils(); const utils = api.useUtils();
@@ -67,7 +69,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
} }
const storedWorkspaceId: string | null = const storedWorkspaceId: string | null =
localStorage.getItem("workspacePublicId"); workspacePublicId ?? localStorage.getItem("workspacePublicId");
if (data.length) { if (data.length) {
const workspaces = data.map(({ workspace, role }) => ({ const workspaces = data.map(({ workspace, role }) => ({
@@ -99,6 +101,11 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
description: selectedWorkspace.workspace.description, description: selectedWorkspace.workspace.description,
role: selectedWorkspace.role, role: selectedWorkspace.role,
}); });
if (workspacePublicId) {
router.push(`/boards`);
localStorage.setItem("workspacePublicId", workspacePublicId);
}
} else { } else {
const primaryWorkspace = data[0]?.workspace; const primaryWorkspace = data[0]?.workspace;
const primaryWorkspaceRole = data[0]?.role; const primaryWorkspaceRole = data[0]?.role;
@@ -114,7 +121,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
role: primaryWorkspaceRole, role: primaryWorkspaceRole,
}); });
} }
}, [data, isLoading]); }, [data, isLoading, workspacePublicId, router]);
return ( return (
<WorkspaceContext.Provider <WorkspaceContext.Provider

View File

@@ -1,5 +1,6 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { authClient } from "@kan/auth/client"; import { authClient } from "@kan/auth/client";
@@ -15,19 +16,30 @@ export default function InvitePage() {
const { code } = router.query; const { code } = router.query;
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [invitationInfo, setInvitationInfo] = useState<any>(null);
const { data: session, isPending: isSessionLoading } = const { data: session, isPending: isSessionLoading } =
authClient.useSession(); authClient.useSession();
const { switchWorkspace } = useWorkspace();
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
const inviteCode = Array.isArray(code) ? code[0] : code;
const acceptInviteMutation = api.member.acceptInviteLink.useMutation({ const acceptInviteMutation = api.member.acceptInviteLink.useMutation({
onSuccess: (result) => { onSuccess: (result) => {
if (result.success) { if (result.success) {
router.push(`/boards`); return router.push(
`/boards?workspacePublicId=${result.workspacePublicId}`,
);
} }
}, },
onError: (error) => { onError: (error) => {
setError(error.message || t`Failed to accept invitation`); if (error.data?.code === "CONFLICT") {
return router.push(`/boards`);
}
setError(
error.message ||
t`Failed to accept invitation. Please try again later, or contact customer support.`,
);
setIsProcessing(false); setIsProcessing(false);
}, },
}); });
@@ -36,40 +48,32 @@ export default function InvitePage() {
data: inviteInfo, data: inviteInfo,
isLoading: isInviteInfoLoading, isLoading: isInviteInfoLoading,
isError: isInviteInfoError, isError: isInviteInfoError,
error: inviteInfoError, } = api.member.getInviteByCode.useQuery(
} = api.member.getInviteByCode.useQuery({ inviteCode: code ?? "" }); { inviteCode: inviteCode ?? "" },
{
// Set invite info when loaded enabled: !!inviteCode,
useEffect(() => { retry: false,
if (inviteInfo) { },
setInvitationInfo(inviteInfo); );
} else if (isInviteInfoError) {
setError(t`Invalid or expired invitation link`);
}
}, [inviteInfo, isInviteInfoError]);
// Auto accept invite if user is logged in // Auto accept invite if user is logged in
useEffect(() => { useEffect(() => {
if (session?.user.id && code && typeof code === "string" && inviteInfo) { if (session?.user.id && inviteCode && inviteInfo && !error) {
handleAcceptInvite(); setIsProcessing(true);
setError(null);
acceptInviteMutation.mutate({
inviteCode,
});
} }
}, [session, code, inviteInfo]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [session?.user.id, inviteCode, inviteInfo, error]);
const handleAcceptInvite = async () => { if (
if (!code || typeof code !== "string" || !session?.user.id) { !isInviteInfoError &&
return; !error &&
} (session?.user.id || isInviteInfoLoading || isSessionLoading)
) {
setIsProcessing(true);
setError(null);
acceptInviteMutation.mutate({
inviteCode: code,
userId: session.user.id,
});
};
if (session?.user.id || isInviteInfoLoading || isSessionLoading) {
return ( return (
<> <>
<PageHead title={t`Join workspace`} /> <PageHead title={t`Join workspace`} />
@@ -81,13 +85,21 @@ export default function InvitePage() {
); );
} }
if (isInviteInfoError || !inviteInfo) { const PageWrapper = ({ children }: { children: React.ReactNode }) => {
return ( return (
<> <>
<PageHead title={t`Join workspace`} /> <PageHead title={t`Join workspace | kan.bn`} />
{children}
</>
);
};
if (isInviteInfoError || (!isInviteInfoLoading && !inviteInfo)) {
return (
<PageWrapper>
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8"> <div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
<PatternedBackground /> <PatternedBackground />
<div className="w-full max-w-md space-y-8"> <div className="z-10 w-full max-w-md space-y-8">
<div> <div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000"> <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
{t`Invalid invitation`} {t`Invalid invitation`}
@@ -103,46 +115,57 @@ export default function InvitePage() {
</div> </div>
</div> </div>
</div> </div>
</> </PageWrapper>
); );
} }
return ( return (
<> <PageWrapper>
<PageHead title={t`Join workspace`} />
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8"> <div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
<PatternedBackground /> <PatternedBackground />
<div className="z-10 w-full max-w-md space-y-8"> <div className="z-10 w-full max-w-[400px] space-y-8">
<div> <div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000"> <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
{t`Join workspace`} {t`Join workspace`}
</h2> </h2>
<p className="mt-4 text-center text-sm text-light-600 dark:text-dark-800"> {!error ? (
{t`You've been invited to join ${invitationInfo?.workspaceName || "a workspace"} on kan.bn`} <p className="mt-4 text-center text-sm text-light-600 dark:text-dark-800">
</p> {isCloudEnv
? t`You've been invited to join a workspace on kan.bn.`
: t`You've been invited to join a workspace.`}
</p>
) : (
<p className="mt-4 text-center text-sm text-red-500">{error}</p>
)}
</div> </div>
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
<Button {session?.user.id ? (
href={`/login?next=/invite/${code}`} <Button href={`/boards`} variant="primary" size="md">
disabled={isProcessing} {t`Go to app`}
variant="primary" </Button>
size="md" ) : (
> <>
{t`Sign In`} <Button
</Button> href={`/login?next=/invite/${inviteCode}`}
<Button disabled={isProcessing}
href={`/signup?next=/invite/${code}`} variant="primary"
disabled={isProcessing} size="md"
variant="primary" >
size="md" {t`Sign In`}
> </Button>
{t`Sign Up`} <Button
</Button> href={`/signup?next=/invite/${inviteCode}`}
disabled={isProcessing}
variant="primary"
size="md"
>
{t`Sign Up`}
</Button>
</>
)}
</div> </div>
</div> </div>
</div> </div>
</> </PageWrapper>
); );
} }