mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
fix: minor invite flow fixes (#28158)
* fix: minor invite flow fixes * invalidate organizations
This commit is contained in:
1 parent
208471ef4c
commit
be74acfad3
4 files changed
+11
-311
No files matched your search
@@ -5,7 +5,6 @@ import toast from 'react-hot-toast'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useOrganizationAcceptInvitationMutation } from 'data/organization-members/organization-invitation-accept-mutation'
|
||||
import { useOrganizationDeleteInvitationMutation } from 'data/organization-members/organization-invitation-delete-mutation'
|
||||
import { useOrganizationInvitationTokenQuery } from 'data/organization-members/organization-invitation-token-query'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -28,40 +27,24 @@ export const OrganizationInvite = () => {
|
||||
isError || (isSuccess && (data.token_does_not_exist || data.expired_token || !data.email_match))
|
||||
|
||||
const organizationName = data?.organization_name ?? name ?? 'an organization'
|
||||
const loginRedirectLink = `/?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
|
||||
const loginRedirectLink = `/sign-in?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
|
||||
|
||||
const { mutate: joinOrganization, isLoading: isJoining } =
|
||||
useOrganizationAcceptInvitationMutation({
|
||||
onSuccess: () => {
|
||||
router.push('/')
|
||||
router.push('/projects')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to join organization: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: declineOrganization, isLoading: isDeclining } =
|
||||
useOrganizationDeleteInvitationMutation({
|
||||
onSuccess: () => {
|
||||
router.push('/')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to decline invitation: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
async function handleJoinOrganization() {
|
||||
if (!slug) return console.error('Slug is required')
|
||||
if (!token) return console.error('Token is required')
|
||||
joinOrganization({ slug, token })
|
||||
}
|
||||
|
||||
async function handleDeclineJoinOrganization() {
|
||||
if (!slug) return console.error('Slug is required')
|
||||
if (!data?.invite_id) return console.error('Invite ID is required')
|
||||
declineOrganization({ slug, id: data.invite_id })
|
||||
}
|
||||
|
||||
return (
|
||||
<Loading active={profile !== undefined && isLoading}>
|
||||
<div className="flex flex-col gap-2 px-6 py-8">
|
||||
@@ -96,18 +79,13 @@ export const OrganizationInvite = () => {
|
||||
)}
|
||||
{isSuccess && (
|
||||
<div className="flex flex-row items-center justify-center gap-3">
|
||||
<Button
|
||||
type="default"
|
||||
loading={isDeclining}
|
||||
disabled={isJoining || isDeclining}
|
||||
onClick={handleDeclineJoinOrganization}
|
||||
>
|
||||
Decline
|
||||
<Button type="default" disabled={isJoining} asChild>
|
||||
<Link href="/projects">Decline</Link>
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isJoining}
|
||||
disabled={isJoining || isDeclining}
|
||||
disabled={isJoining}
|
||||
onClick={handleJoinOrganization}
|
||||
icon={<CheckSquare />}
|
||||
>
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import { invalidateOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import type { ResponseError } from 'types'
|
||||
|
||||
export type OrganizationAcceptInvitationVariables = {
|
||||
@@ -35,12 +36,16 @@ export const useOrganizationAcceptInvitationMutation = ({
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
OrganizationMemberUpdateData,
|
||||
ResponseError,
|
||||
OrganizationAcceptInvitationVariables
|
||||
>((vars) => acceptOrganizationInvitation(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
await invalidateOrganizationsQuery(queryClient)
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { OrganizationInvite } from 'components/interfaces/OrganizationInvite/OrganizationInvite'
|
||||
import { OrganizationInviteOld } from 'components/interfaces/OrganizationInvite/OrganizationInviteOld'
|
||||
import { useIsOptedIntoProjectLevelPermissions } from 'hooks/ui/useFlag'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const JoinOrganizationPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { slug } = useParams()
|
||||
const isOptedIntoProjectLevelPermissions = useIsOptedIntoProjectLevelPermissions(slug as string)
|
||||
|
||||
// [Joshen] Can remove this once API changes are deployed to production
|
||||
// To double check whether existing invite flow works with the new endpoints
|
||||
if (!isOptedIntoProjectLevelPermissions) {
|
||||
return <OrganizationInviteOld />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user