mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
273 lines
8.2 KiB
TypeScript
273 lines
8.2 KiB
TypeScript
import { CheckSquare } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
import toast from 'react-hot-toast'
|
|
|
|
import { useParams } from 'common'
|
|
import { useOrganizationJoinDeclineMutation } from 'data/organizations/organization-join-decline-mutation'
|
|
import { useOrganizationJoinMutation } from 'data/organizations/organization-join-mutation'
|
|
import {
|
|
TokenInfo,
|
|
validateTokenInformation,
|
|
} from 'data/organizations/organization-join-token-validation-query'
|
|
import { useSignOut } from 'lib/auth'
|
|
import { useProfile } from 'lib/profile'
|
|
import { Button, Loading } from 'ui'
|
|
|
|
export const OrganizationInviteOld = () => {
|
|
const router = useRouter()
|
|
const signOut = useSignOut()
|
|
const { profile } = useProfile()
|
|
const { slug, token, name } = useParams()
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [error, setError] = useState<any>()
|
|
const [tokenValidationInfo, setTokenValidationInfo] = useState<TokenInfo>(undefined)
|
|
const [tokenInfoLoaded, setTokenInfoLoaded] = useState(false)
|
|
|
|
const {
|
|
token_does_not_exist,
|
|
sso_mismatch,
|
|
email_match,
|
|
expired_token,
|
|
organization_name,
|
|
invite_id,
|
|
} = tokenValidationInfo || {}
|
|
|
|
const loginRedirectLink = `/?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
|
|
|
|
const { mutate: joinOrganization } = useOrganizationJoinMutation({
|
|
onSuccess: () => {
|
|
setIsSubmitting(false)
|
|
router.push('/')
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to join organization: ${error.message}`)
|
|
setIsSubmitting(false)
|
|
},
|
|
})
|
|
|
|
const { mutate: declineOrganization } = useOrganizationJoinDeclineMutation({
|
|
onSuccess: () => {
|
|
setIsSubmitting(false)
|
|
router.push('/')
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to decline invitation: ${error.message}`)
|
|
setIsSubmitting(false)
|
|
},
|
|
})
|
|
|
|
const isError =
|
|
error ||
|
|
!!(tokenInfoLoaded && sso_mismatch) ||
|
|
!!(tokenInfoLoaded && token_does_not_exist) ||
|
|
(tokenInfoLoaded && !email_match) ||
|
|
(tokenInfoLoaded && expired_token)
|
|
|
|
const ErrorMessage = () => {
|
|
const Container = ({ children }: { children: React.ReactNode }) => (
|
|
<div
|
|
className={[
|
|
'flex flex-col items-center justify-center gap-3 text-sm',
|
|
isError ? 'text-foreground-light' : 'text-foreground',
|
|
].join(' ')}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
|
|
const message = error ? (
|
|
<p>There was an error requesting details for this invitation. ({error.message})</p>
|
|
) : sso_mismatch ? (
|
|
<>
|
|
<p>SSO providers do not match.</p>
|
|
<p className="text-foreground-lighter">
|
|
This invitation comes from a different identity provider than the one you are currently
|
|
logged in with. Please use the same SSO provider instead.
|
|
</p>
|
|
</>
|
|
) : token_does_not_exist ? (
|
|
<>
|
|
<p>The invite token is invalid.</p>
|
|
<p className="text-foreground-lighter">
|
|
Try copying and pasting the link from the invite email, or ask the organization owner to
|
|
invite you again.
|
|
</p>
|
|
</>
|
|
) : !email_match ? (
|
|
<>
|
|
<p>
|
|
Your email address {profile?.primary_email} does not match the email address this
|
|
invitation was sent to.
|
|
</p>
|
|
<p className="text-foreground-lighter">
|
|
To accept this invitation, you will need to{' '}
|
|
<a
|
|
className="cursor-pointer text-brand"
|
|
onClick={async () => {
|
|
await signOut()
|
|
router.reload()
|
|
}}
|
|
>
|
|
sign out
|
|
</a>{' '}
|
|
and then sign in or create a new account using the same email address used in the
|
|
invitation.
|
|
</p>
|
|
</>
|
|
) : expired_token ? (
|
|
<>
|
|
<p>The invite token has expired.</p>
|
|
<p className="text-foreground-lighter">
|
|
Please request a new one from the organization owner.
|
|
</p>
|
|
</>
|
|
) : (
|
|
''
|
|
)
|
|
|
|
return isError ? <Container>{message}</Container> : null
|
|
}
|
|
|
|
const Content = () => (
|
|
<>
|
|
<div className="flex flex-col gap-2 px-6 py-8">
|
|
<>
|
|
<p className="text-sm text-foreground">You have been invited to join </p>
|
|
{organization_name ? (
|
|
<>
|
|
<p className="text-3xl text-foreground">
|
|
{name ? name : organization_name ? `${organization_name}` : 'an organization'}
|
|
</p>
|
|
{!token_does_not_exist && (
|
|
<p className="text-sm text-foreground-lighter">an organization on Supabase</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="text-3xl text-foreground">{'an organization'}</p>
|
|
</>
|
|
)}
|
|
{slug && (
|
|
<p className="text-xs text-foreground-lighter">{`organization slug: ${slug}`}</p>
|
|
)}
|
|
</>
|
|
</div>
|
|
|
|
<div
|
|
className={['border-t border-muted', isError ? 'bg-alternative' : 'bg-transparent'].join(
|
|
' '
|
|
)}
|
|
>
|
|
<div className="flex flex-col gap-4 px-6 py-4 ">
|
|
{profile && !isError && (
|
|
<div className="flex flex-row items-center justify-center gap-3">
|
|
<Button onClick={handleDeclineJoinOrganization} htmlType="submit" type="default">
|
|
Decline
|
|
</Button>
|
|
<Button
|
|
onClick={handleJoinOrganization}
|
|
htmlType="submit"
|
|
loading={isSubmitting}
|
|
type="primary"
|
|
icon={<CheckSquare />}
|
|
>
|
|
Join organization
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{tokenInfoLoaded && <ErrorMessage />}
|
|
|
|
{!profile && (
|
|
<div className="flex flex-col gap-3">
|
|
<p className="text-xs text-foreground-lighter">
|
|
You will need to sign in to accept this invitation
|
|
</p>
|
|
<div className="flex justify-center gap-3">
|
|
<Button asChild type="default">
|
|
<Link href={loginRedirectLink}>Sign in</Link>
|
|
</Button>
|
|
<Button asChild type="default">
|
|
<Link href={loginRedirectLink}>Create an account</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
|
|
async function handleJoinOrganization() {
|
|
if (!slug) return console.error('Slug is required')
|
|
if (!token) return console.error('Token is required')
|
|
setIsSubmitting(true)
|
|
joinOrganization({ slug, token })
|
|
}
|
|
|
|
async function handleDeclineJoinOrganization() {
|
|
if (!slug) return console.error('Slug is required')
|
|
if (!invite_id) return console.error('Invite ID is required')
|
|
setIsSubmitting(true)
|
|
declineOrganization({ slug, invited_id: invite_id })
|
|
}
|
|
|
|
useEffect(() => {
|
|
const fetchTokenInfo = async () => {
|
|
if (!slug) return console.error('Slug is required')
|
|
if (!token) return console.error('Token is required')
|
|
|
|
try {
|
|
const response = await validateTokenInformation({ slug, token })
|
|
setTokenInfoLoaded(true)
|
|
setTokenValidationInfo(response)
|
|
} catch (error) {
|
|
setError(error)
|
|
setTokenInfoLoaded(true)
|
|
}
|
|
}
|
|
|
|
if (!tokenInfoLoaded && token) {
|
|
fetchTokenInfo()
|
|
}
|
|
|
|
/**
|
|
* if params are empty then redirect
|
|
* user to the homepage of app
|
|
*/
|
|
// if (!slug && !token) router.push('/')
|
|
}, [token, router.asPath])
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
'flex h-full min-h-screen bg-studio',
|
|
'w-full flex-col place-items-center',
|
|
'items-center justify-center gap-8 px-5',
|
|
].join(' ')}
|
|
>
|
|
<Link href="/projects" className="flex items-center justify-center gap-4">
|
|
<img
|
|
src={`${router.basePath}/img/supabase-logo.svg`}
|
|
alt="Supabase"
|
|
className="block h-[24px] cursor-pointer rounded"
|
|
/>
|
|
</Link>
|
|
<div
|
|
className="
|
|
mx-auto overflow-hidden rounded-md border
|
|
border-muted bg-alternative text-center shadow
|
|
md:w-[400px]
|
|
"
|
|
>
|
|
<Loading active={!tokenInfoLoaded}>
|
|
<Content />
|
|
</Loading>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|