fix: minor invite flow fixes (#28158)

* fix: minor invite flow fixes

* invalidate organizations
This commit is contained in:
Alaister Young authored and GitHub committed 2024-07-24 16:13:26 +08:00
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) {
-11
View File
@@ -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