From 9660b0075c877e3bd8eaa4aed152a504c759e936 Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Wed, 13 May 2026 10:40:41 +1000
Subject: [PATCH 1/2] refine organisation invite state helpers (#45813)
## What kind of change does this PR introduce?
Code cleanup. Follow-up to #45774.
## What is the current behavior?
The organisation invite interstitial derives invite states, titles, and
descriptions from nested conditional logic in the component. That makes
the component harder to scan and pushes too much state coverage into
render tests.
## What is the new behavior?
See #45774 for screenshots of the general UI before-and-after (which
this one builds upon). That PR also contains testing instructions.
Extracts the invite status and content decisions into small pure
helpers, then covers those helpers with focused unit tests.
The component keeps the user-facing render and interaction coverage,
including the invalid lookup regression where a 404 should render the
invalid invite state instead of raw backend copy.
## Summary by CodeRabbit
* **Refactor**
* Improved organization invite flow with enhanced error state handling
for expired, invalid, and wrong-account scenarios.
* Better consistency in error messages and user guidance throughout the
invite process.
[](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45813)
---
.../OrganizationInvite/OrganizationInvite.tsx | 77 ++++-------
.../OrganizationInvite.utils.ts | 96 +++++++++++++
.../components/OrganizationInvite.test.tsx | 34 -----
.../OrganizationInvite.utils.test.ts | 126 ++++++++++++++++++
4 files changed, 251 insertions(+), 82 deletions(-)
create mode 100644 apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts
create mode 100644 apps/studio/tests/components/OrganizationInvite.utils.test.ts
diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
index cb8fcc1499508..214702fd3f750 100644
--- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
+++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
@@ -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,
@@ -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({
@@ -107,15 +88,15 @@ export const OrganizationInvite = () => {
title={
isInvitationLoading ? (
Enter this verification code on Supabase CLI to authorize login.
-After authorizing the login attempt, you can close this window.
-- If you ever want to remove your new token, go to{' '} - - Access Tokens - {' '} - page. -
-
+ After authorizing, you can close this tab or manage tokens like this one in{' '}
+