Files
Charis 39e21ebef6 fix(studio): flash of sign in for join organization page (#41283)
The join organization page flashes the sign in screen briefly because it
does not check for loading status. Fixed so it goes directly from
loading skeleton to join confirmation view.

Resolves FE-2220
2025-12-11 20:14:07 +00:00

152 lines
5.4 KiB
TypeScript

import { CheckSquare } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { useIsLoggedIn, useParams } from 'common'
import { useOrganizationAcceptInvitationMutation } from 'data/organization-members/organization-invitation-accept-mutation'
import { useOrganizationInvitationTokenQuery } from 'data/organization-members/organization-invitation-token-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useProfile } from 'lib/profile'
import { Button, cn } from 'ui'
import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
import { OrganizationInviteError } from './OrganizationInviteError'
export const OrganizationInvite = () => {
const router = useRouter()
const isLoggedIn = useIsLoggedIn()
const { profile, isLoading: isLoadingProfile } = useProfile()
const { slug, token } = useParams()
const isSignUpEnabled = useIsFeatureEnabled('dashboard_auth:sign_up')
const {
data,
error,
isSuccess: isSuccessInvitation,
isError: isErrorInvitation,
isPending: isLoadingInvitation,
} = useOrganizationInvitationTokenQuery(
{ slug, token },
{
retry: false,
refetchOnWindowFocus: false,
enabled: !!profile,
}
)
const hasError =
isErrorInvitation ||
(isSuccessInvitation && (data.token_does_not_exist || data.expired_token || !data.email_match))
const inviteIsNoLongerValid =
error?.code === 401 && error?.message.includes('Failed to retrieve organization')
const organizationName = isSuccessInvitation ? data?.organization_name : 'An organization'
const loginRedirectLink = `/sign-in?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const signupRedirectLink = `/sign-up?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const { mutate: joinOrganization, isPending: isJoining } =
useOrganizationAcceptInvitationMutation({
onSuccess: () => {
router.push('/organizations')
},
onError: (error) => {
toast.error(`Failed to join organization: ${error.message}`)
},
})
async function handleJoinOrganization() {
if (!slug) return console.error('Slug is required')
if (!token) return console.error('Token is required')
joinOrganization({ slug, token })
}
return (
<div
className={cn(
'mx-auto overflow-hidden rounded-md border',
'border-muted bg-alternative text-center shadow',
'md:w-[400px]'
)}
>
{!isLoggedIn || (!profile && !isLoadingProfile) ? (
<>
<Admonition
showIcon={false}
type="default"
title={`Sign in${isSignUpEnabled ? ' or create an account' : ''} first to view this invitation`}
className="border-0 rounded-none text-left"
/>
<div className="p-4 border-muted border-t flex gap-x-3 justify-center">
<Button asChild type="default">
<Link href={loginRedirectLink}>Sign in</Link>
</Button>
{isSignUpEnabled && (
<Button asChild type="default">
<Link href={signupRedirectLink}>Create an account</Link>
</Button>
)}
</div>
</>
) : isLoadingProfile || isLoadingInvitation ? (
<div className="p-5">
<GenericSkeletonLoader />
</div>
) : inviteIsNoLongerValid ? (
<>
<Admonition
type="default"
title="Invalid invitation"
description="This organization invite is no longer valid as it has either been accepted or declined"
className="border-0 rounded-none text-left"
/>
<div className="p-4 border-muted border-t">
<Button type="default" asChild>
<Link href="/">Back to dashboard</Link>
</Button>
</div>
</>
) : (
<>
<div className="flex flex-col gap-y-1 px-6 py-6">
<p className="text-sm text-foreground-light">You have been invited to join </p>
<p className="text-foreground text-2xl">{organizationName}</p>
{isSuccessInvitation && (
<p className="text-xs text-foreground-lighter">{`Organization slug: ${slug}`}</p>
)}
</div>
<div
className={cn('border-t border-muted', hasError ? 'bg-alternative' : 'bg-transparent')}
>
<div
className={cn(
'flex flex-col gap-4',
!isLoadingInvitation && !hasError && 'px-6 py-4'
)}
>
{hasError && (
<OrganizationInviteError data={data} error={error} isError={isErrorInvitation} />
)}
{isSuccessInvitation && !hasError && (
<div className="flex flex-row items-center justify-center gap-3">
<Button type="default" disabled={isJoining} asChild>
<Link href="/projects">Decline</Link>
</Button>
<Button
type="primary"
loading={isJoining}
disabled={isJoining}
onClick={handleJoinOrganization}
icon={<CheckSquare />}
>
Join organization
</Button>
</div>
)}
</div>
</div>
</>
)}
</div>
)
}