Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { toast } from 'sonner'
import { Button, Card, CardContent } from 'ui'
import { Admonition, ShimmeringLoader } from 'ui-patterns'

import {
getOrganizationInviteContent,
getOrganizationInviteStatus,
} from './OrganizationInvite.utils'
import { OrganizationInviteError } from './OrganizationInviteError'
import {
InterstitialAccountRow,
Expand Down Expand Up @@ -40,50 +44,27 @@ export const OrganizationInvite = () => {
enabled: !!profile && !!slug && !!token,
}
)
const inviteIsNoLongerValid =
error?.code === 401 && error?.message.includes('Failed to retrieve organization')
const inviteIsInvalid =
(isSuccessInvitation && !!data?.token_does_not_exist) ||
(isErrorInvitation && error?.code === 404)
const hasError =
isErrorInvitation ||
(isSuccessInvitation && (data.token_does_not_exist || data.expired_token || !data.email_match))

const isWrongAccount = isSuccessInvitation && !!data && !data.email_match
const showOrganizationHeader =
isSuccessInvitation &&
!!data &&
!data.token_does_not_exist &&
!data.expired_token &&
!isWrongAccount
const organizationName = data?.organization_name ?? 'an organization'
const isSignedOut = !isLoggedIn || (!profile && !isLoadingProfile)
const isInvitationLoading =
!isSignedOut && (isLoadingProfile || isLoadingInvitation || !router.isReady)
const inviteStatus = getOrganizationInviteStatus({
data,
error,
isErrorInvitation,
isLoadingInvitation,
isLoadingProfile,
isLoggedIn,
isRouterReady: router.isReady,
isSuccessInvitation,
profileExists: !!profile,
})
const isSignedOut = inviteStatus === 'signed-out'
const isInvitationLoading = inviteStatus === 'loading'
const inviteContent = getOrganizationInviteContent({
data,
isSignUpEnabled,
status: inviteStatus,
})
const hasError = ['wrong-account', 'expired', 'invalid', 'error'].includes(inviteStatus)
const loginRedirectLink = `/sign-in?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const signupRedirectLink = `/sign-up?returnTo=${encodeURIComponent(`/join?token=${token}&slug=${slug}`)}`
const interstitialTitle = inviteIsNoLongerValid
? 'Invite no longer available'
: isSignedOut
? 'View invitation'
: isWrongAccount
? 'Wrong account'
: inviteIsInvalid
? 'Invite invalid'
: isErrorInvitation
? 'Unable to load invitation'
: data?.expired_token
? 'Invite expired'
: showOrganizationHeader
? `Join ${organizationName}`
: undefined
const interstitialDescription = showOrganizationHeader
? isSignedOut
? `Sign in${isSignUpEnabled ? ' or create an account' : ''} to view this invitation`
: 'You have been invited to join this Supabase organization'
: isSignedOut
? `Sign in${isSignUpEnabled ? ' or create an account' : ''} to view this invitation`
: undefined

const { mutate: joinOrganization, isPending: isJoining } =
useOrganizationAcceptInvitationMutation({
Expand All @@ -107,15 +88,15 @@ export const OrganizationInvite = () => {
title={
isInvitationLoading ? (
<ShimmeringLoader className="mx-auto h-7 w-36 max-w-full py-0" />
) : interstitialTitle ? (
interstitialTitle
) : inviteContent.title ? (
inviteContent.title
) : undefined
}
description={
isInvitationLoading ? (
<ShimmeringLoader className="mx-auto h-4 w-48 max-w-full py-0" />
) : interstitialDescription ? (
interstitialDescription
) : inviteContent.description ? (
inviteContent.description
) : undefined
}
titleClassName="text-xl"
Expand Down Expand Up @@ -159,7 +140,7 @@ export const OrganizationInvite = () => {
)
}

if (inviteIsNoLongerValid) {
if (inviteStatus === 'no-longer-valid') {
return withLayout(
<div className="flex flex-col gap-3">
<Admonition
Expand All @@ -179,7 +160,7 @@ export const OrganizationInvite = () => {
data={data}
error={error}
isError={isErrorInvitation}
isInvalidInvite={inviteIsInvalid}
isInvalidInvite={inviteStatus === 'invalid'}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { OrganizationInviteByToken } from '@/data/organization-members/organization-invitation-token-query'
import type { ResponseError } from '@/types'

type OrganizationInviteStatusVariables = {
data?: OrganizationInviteByToken
error?: ResponseError | null
isErrorInvitation: boolean
isLoadingInvitation: boolean
isLoadingProfile: boolean
isLoggedIn: boolean
isRouterReady: boolean
isSuccessInvitation: boolean
profileExists: boolean
}

type OrganizationInviteContentVariables = {
data?: OrganizationInviteByToken
isSignUpEnabled: boolean
status: OrganizationInviteStatus
}

export type OrganizationInviteStatus =
| 'signed-out'
| 'loading'
| 'ready'
| 'wrong-account'
| 'expired'
| 'invalid'
| 'no-longer-valid'
| 'error'

export function getOrganizationInviteStatus({
data,
error,
isErrorInvitation,
isLoadingInvitation,
isLoadingProfile,
isLoggedIn,
isRouterReady,
isSuccessInvitation,
profileExists,
}: OrganizationInviteStatusVariables): OrganizationInviteStatus {
const isSignedOut = !isLoggedIn || (!profileExists && !isLoadingProfile)

if (isSignedOut) return 'signed-out'
if (isLoadingProfile || isLoadingInvitation || !isRouterReady) return 'loading'

if (error?.code === 401 && error?.message.includes('Failed to retrieve organization')) {
return 'no-longer-valid'
}

if (
(isSuccessInvitation && !!data?.token_does_not_exist) ||
(isErrorInvitation && error?.code === 404)
) {
return 'invalid'
}

if (isErrorInvitation) return 'error'
if (isSuccessInvitation && !!data?.expired_token) return 'expired'
if (isSuccessInvitation && !!data && !data.email_match) return 'wrong-account'

return 'ready'
}

export function getOrganizationInviteContent({
data,
isSignUpEnabled,
status,
}: OrganizationInviteContentVariables) {
const signedOutDescription = `Sign in${
isSignUpEnabled ? ' or create an account' : ''
} to view this invitation`

if (status === 'signed-out') {
return {
title: 'View invitation',
description: signedOutDescription,
}
}

if (status === 'ready') {
return {
title: `Join ${data?.organization_name ?? 'an organization'}`,
description: 'You have been invited to join this Supabase organization',
}
}

if (status === 'wrong-account') return { title: 'Wrong account' }
if (status === 'expired') return { title: 'Invite expired' }
if (status === 'invalid') return { title: 'Invite invalid' }
if (status === 'no-longer-valid') return { title: 'Invite no longer available' }
if (status === 'error') return { title: 'Unable to load invitation' }

return {}
}
9 changes: 9 additions & 0 deletions apps/studio/components/layouts/InterstitialLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { motion } from 'framer-motion'
import { ArrowRightLeft } from 'lucide-react'
import type { PropsWithChildren, ReactNode } from 'react'
import { Card, CardContent, CardHeader, cn } from 'ui'

Expand Down Expand Up @@ -88,6 +89,14 @@ export const LogoBox = ({ children, className }: { children: ReactNode; classNam
</div>
)

export const LogoPair = ({ left, right }: { left: ReactNode; right: ReactNode }) => (
<div className="flex items-center justify-center gap-2.5">
{left}
<ArrowRightLeft className="size-4 text-foreground-muted" />
{right}
</div>
)

export const SupabaseLogo = () => (
<LogoBox>
<img alt="Supabase" src={`${BASE_PATH}/img/supabase-logo.svg`} className="size-7" />
Expand Down
15 changes: 13 additions & 2 deletions apps/studio/components/ui/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
onClick,
copyLabel = 'Copy',
copiedLabel = 'Copied',
type = 'primary',
icon,
className,
...props
},
ref
Expand All @@ -53,9 +56,17 @@ const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
onClick?.(e)
}}
{...props}
className={cn({ 'px-1': iconOnly }, props.className)}
type={type}
className={cn({ 'px-1': iconOnly }, className)}
icon={
showCopied ? <Check strokeWidth={2} className="text-brand" /> : (props.icon ?? <Copy />)
showCopied ? (
<Check
strokeWidth={2}
className={cn(type === 'primary' ? 'text-inherit' : 'text-brand')}
/>
) : (
(icon ?? <Copy />)
)
}
>
{!iconOnly && <>{children ?? (showCopied ? copiedLabel : copyLabel)}</>}
Expand Down
Loading
Loading