mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
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
152 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|