Files
supabase/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts
Danny White 5edcaef74c chore: show organization invite errors inline (#48470)
## What kind of change does this PR introduce?

Bug fix and design-system documentation update.

## What is the current behavior?

Invite acceptance failures only appear in a transient toast.

## What is the new behavior?

Invite failures remain visible beside the actions. The design-system
guidance now distinguishes field, action, state, and toast feedback.

| Before | After |
| --- | --- |
| <img width="759" height="619" alt="Join Organization Supabase"
src="https://github.com/user-attachments/assets/ed8e974c-5da3-477a-81da-628d3f847131"
/> | <img width="741" height="768" alt="Join Organization Supabase"
src="https://github.com/user-attachments/assets/4c3f6bcd-4ed9-40b2-8280-e8c8a44ecbd6"
/> |

## To test

With local Studio running at `http://localhost:8082`:

1. Open
`apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts`.
2. At line 37, immediately inside `getOrganizationInviteStatus`, add:
   ```tsx
   return 'ready'
   ```
This deliberately bypasses invite lookup and account checks for the
visual test.
3. Open
`apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx`.
4. At line 30, change:
   ```tsx
   const [joinError, setJoinError] = useState<string>()
   ```
   to:
   ```tsx
const [joinError, setJoinError] = useState<string>('Invite token can
only be accepted via an SSO account')
   ```
5. Open `http://localhost:8082/join?token=test&slug=test` while signed
in.
6. Confirm the card says **Join an organization** and shows the error
below **Decline**, separated from the actions by a divider.
7. Revert both temporary edits before committing anything.

## Additional context

First PR in a five-PR stack.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a new connect interstitial example showcasing an inline
action-error state with clear retry guidance.

- **Bug Fixes**
- Invitation acceptance failures now show inline destructive feedback
under “Accept invite,” keeping the button enabled for retry (and
removing prior toast-based failure behavior).
  - Updated the invalid-invitation title to “Invalid invitation.”
  - Changed the “Decline” link destination to `/organizations`.

- **Documentation**
  - Expanded Sonner toast “When to use” guidance.
- Refined form and connect interstitial action-feedback patterns (inline
vs toast usage).

- **Tests**
- Updated and added coverage for the inline error rendering and “Invalid
invitation” text.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-07-31 06:23:24 +10:00

104 lines
2.8 KiB
TypeScript

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
}
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,
error,
isSignUpEnabled,
status,
}: {
data?: OrganizationInviteByToken
error?: ResponseError | null
isSignUpEnabled: boolean
status: OrganizationInviteStatus
}) {
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: 'Invalid invitation' }
if (status === 'no-longer-valid') return { title: 'Invite no longer available' }
if (status === 'error') {
if (error?.message.includes('MFA required')) {
return {
title: 'Set up MFA for your account',
description: 'MFA needs to be enabled on your account to join this organization',
}
} else return { title: 'Unable to load invitation' }
}
return {}
}