From f95d978cbc2ce4e794e0b475d864e7fd705e302d Mon Sep 17 00:00:00 2001
From: Chris Chinchilla
+ Projected costs at the end of the billing cycle. Includes predictable
+ costs for Compute Hours, IPv4, Custom Domain and Point-In-Time-Recovery,
+ but no costs for metrics like MAU, storage or function invocations. Final
+ amounts may vary depending on your usage.
+
+ Estimated based on your organization's billing address. The final
+ amount may be adjusted at the end of the billing cycle.
+
+ We were unable to estimate tax for your organization. Please verify your
+ billing address in your organization settings.
+ Visit here to log in. Hello {{ .Data.first_name }}, please confirm your signup.
+ {label} ({items.length})
+
+ {items[0].name} ({items[0].orgName})
+
## Test plan
- [x] On an org with prepaid credits applied, verify the "Prepaid
Credits" row appears below Current Costs with the negative amount and a
tooltip showing the backend description
- [x] On an org without prepaid credits, verify the row is not rendered
## Summary by CodeRabbit
## Summary by CodeRabbit
* **Improvements**
* Clarified the "Upcoming Invoice" explanatory copy for non–AWS
Marketplace-managed organizations: it now notes the invoice will
continue updating until the end of your billing cycle.
* Improved invoice breakdown: compute charges and compute credits are
distinguished, and a dedicated "Prepaid Credits" row is shown before
projected costs for clearer billing totals.
---
.../BillingBreakdown/BillingBreakdown.tsx | 5 +-
.../BillingBreakdown/UpcomingInvoice.tsx | 123 +++++++++---------
2 files changed, 64 insertions(+), 64 deletions(-)
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx
index 9eeebc2b440..cf3b15f16db 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx
@@ -84,9 +84,8 @@ const BillingBreakdown = () => {
>
) : (
<>
- Your upcoming invoice (excluding credits) will continue to update until the end of
- your billing cycle on {billingCycleEnd.format('MMMM DD')}. For a more detailed
- breakdown, visit the{' '}
+ Your upcoming invoice will continue to update until the end of your billing cycle on{' '}
+ {billingCycleEnd.format('MMMM DD')}. For a more detailed breakdown, visit the{' '}
## Summary by CodeRabbit
* **New Features**
* Billing breakdown now detects when the plan fee was already paid
upfront, hides the redundant plan line, and notes only usage will be
invoiced; shows the organization plan name when available.
* Backup restoration: added an optional recovery time target for
physical backups.
* Expanded supported AWS instance types for deployments.
* **UI**
* Compute and Replica Compute docs links now use inline linking for a
smoother in-app experience.
[](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45765)
---------
Co-authored-by: Joshen Lim
## Changes
- **Detection hook** (`useFlyDeprecationProjects`): reads only from
already-cached data — `useSelectedProjectQuery` for the current project,
plus `useOrgProjectsInfiniteQuery` scoped to the selected org. Zero
cross-org fan-out: worst case is one paginated query per session (the
same one the project list page already makes).
- **Banner component** (`FlyDeprecationBanner.tsx`): mounted in
`AppBannerWrapper`. Dynamic title (primaries / branches / both), dialog
lists affected projects with org name, numbered migration steps, links
to backup/restore CLI + Dashboard backup + branching docs. List
truncates to 5 entries with "…and N more." tail when more are affected.
- **Telemetry**: `fly_deprecation_banner_exposed` and
`fly_deprecation_banner_dismissed` events emitted via `useTrack`
(auto-injects project + org groups). Properties: `primaryCount`,
`branchCount`. CTA click tracking intentionally omitted — migration
outcome is measured via warehouse `cloud_provider = 'FLY'` decay.
- **LocalStorage**: dated dismissal key `FLY_DEPRECATION_2026_05_31`;
orphan `FLY_POSTGRES_DEPRECATION_WARNING` from PR #33510 removed in the
same change so users who dismissed the Feb 2025 banner still see this
one.
- **Support contact**: email `success@supabase.io` only (no support
ticket link), per Brian's outreach copy in the Linear issues.
## Coverage trade-off
Banner renders on project pages (selected-project check) and pages where
the selected org's projects list is cached (org overview, project list).
It does **not** render on `/dashboard` home or other pages without org
context. Email outreach from GROWTH-817 / GROWTH-819 handles those
users. This was a deliberate trade-off to avoid cross-org fan-out load.
## Lifecycle
Banner expires `2026-06-01T00:00:00Z` (right after the May 31 deadline).
Stale client bundles stop rendering it without a redeploy. Cleanup PR
planned post-deadline to remove the component, hook, localStorage key,
and telemetry events.
## Testing
Tested on the Vercel preview with React Query cache overrides to mock a
Fly project:
- [x] Banner renders for a user with at least one project where
`cloud_provider === 'FLY'`
- [x] Banner does **not** render for a user with no Fly projects
- [x] Banner does **not** render on `/sign-in`
- [x] Title varies by primaries-only / branches-only / both
- [x] Dialog lists affected projects with org name in parens
- [x] Dialog list truncates to 5 with "…and N more." for larger sets
- [x] Migration guide / Dashboard backup / branching links open in a new
tab
- [x] Dismiss (×) closes the banner and persists across hard reload
(localStorage `fly-deprecation-2026-05-31-dismissed`)
- [x] PostHog receives one `fly_deprecation_banner_exposed` per mount
with `primaryCount` + `branchCount` and `$groups.organization` populated
- [x] PostHog receives one `fly_deprecation_banner_dismissed` on close
with the same property shape
## Linear
- fixes GROWTH-817
- fixes GROWTH-819
---
.../interfaces/App/AppBannerWrapper.tsx | 2 +
.../AppLayout/FlyDeprecationBanner.tsx | 222 ++++++++++++++++++
.../hooks/misc/useFlyDeprecationProjects.ts | 62 +++++
packages/common/constants/local-storage.ts | 2 +-
packages/common/telemetry-constants.ts | 32 +++
5 files changed, 319 insertions(+), 1 deletion(-)
create mode 100644 apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx
create mode 100644 apps/studio/hooks/misc/useFlyDeprecationProjects.ts
diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
index b59ad529fab..22a4cb613c1 100644
--- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
+++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
@@ -3,6 +3,7 @@ import { PropsWithChildren } from 'react'
import { OrganizationResourceBanner } from '../Organization/HeaderBanner'
import { ClockSkewBanner } from '@/components/layouts/AppLayout/ClockSkewBanner'
+import { FlyDeprecationBanner } from '@/components/layouts/AppLayout/FlyDeprecationBanner'
import { NoticeBanner, NoticeBanner2 } from '@/components/layouts/AppLayout/NoticeBanner'
import { StatusPageBanner } from '@/components/layouts/AppLayout/StatusPageBanner'
@@ -17,6 +18,7 @@ export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
+ {visible.map((p) => (
+
+ )}
+
- Project last paused on{' '}
-
From 791fc744125500e7302b80b6c253e91b31caae1a Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Tue, 12 May 2026 10:41:47 +1000
Subject: [PATCH 12/98] feat(studio): shared connect layout for organisation
invites (#45774)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What kind of change does this PR introduce?
Feature. Part of DEPR-279.
## What is the current behavior?
The organization invite page has its own bespoke centered card and
page-level Supabase logo.
## What is the new behavior?
Introduces a minimal shared interstitial layout and migrates `/join`
onto it as the first small connect-surface slice. The invite API and
accept-invite mutation paths are unchanged.
| Before | After |
| --- | --- |
| |
|
|
|
|
|
|
|
|
|
|
## Testing notes
Staging invite emails are generated with the fixed staging dashboard
origin, for example:
```text
https://supabase.green/dashboard/join?token=...&slug=...
```
To test this PR preview with a real invite token, keep the path and
query string from the email but replace the origin with the Vercel
preview origin, for example:
```text
https://studio-staging-git-dnywh-featconnect-interstitial-join-supabase.vercel.app/dashboard/join?token=...&slug=...
```
### Manual state checks
- **Signed out:** open the swapped invite URL in an incognito window or
a browser signed out of Studio. Expected: `View invitation`,
sign-in/create-account actions, and no loading skeleton hang.
- **Wrong account:** sign in to the PR preview as an account that is not
the invite recipient, then open the swapped invite URL. Expected: `Wrong
account`, warning callout, and `Sign out`.
- **Happy path:** sign in as the invited email address, then open the
swapped invite URL. Expected: `Join {Organization}`, signed-in account
row, `Accept invite`, and `Decline`. Accepting should join the
organization.
- **Invalid token:** alter one character in the token in the swapped
invite URL. Expected: invalid invite state.
- **No longer valid:** accept the invite once, then open the same
swapped invite URL again. Expected: no-longer-valid/already-used state,
depending on the backend response.
### Test-covered states
Expired invites, generic backend error, loading, and
create-account-disabled states are harder to force manually in staging.
They are covered by `tests/components/OrganizationInvite.test.tsx`.
## Summary by CodeRabbit
* **Refactor**
* Redesigned the organization invitation experience with an interstitial
layout, clearer early-return flows for signed-out, loading,
expired/invalid, wrong-account, and accepted-invite states; primary CTA
now reads “Accept invite”.
* Streamlined error and sign-out flows with clearer, focused messaging.
* **New Features**
* Added a reusable interstitial layout and compact account row for
invitation screens.
* **Tests**
* Added comprehensive tests covering invite states, accept/decline
actions, and error handling.
[](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45774)
---
.../OrganizationInvite/OrganizationInvite.tsx | 240 +++++++++------
.../OrganizationInviteError.tsx | 117 +++++---
.../components/layouts/InterstitialLayout.tsx | 119 ++++++++
apps/studio/pages/join.tsx | 42 +--
.../components/OrganizationInvite.test.tsx | 283 ++++++++++++++++++
5 files changed, 626 insertions(+), 175 deletions(-)
create mode 100644 apps/studio/components/layouts/InterstitialLayout.tsx
create mode 100644 apps/studio/tests/components/OrganizationInvite.test.tsx
diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
index dd4101c5e45..cb8fcc14995 100644
--- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
+++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx
@@ -1,21 +1,27 @@
import { useIsLoggedIn, useParams } from 'common'
-import { CheckSquare } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
+import type { ReactNode } from 'react'
import { toast } from 'sonner'
-import { Button, cn } from 'ui'
-import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
+import { Button, Card, CardContent } from 'ui'
+import { Admonition, ShimmeringLoader } from 'ui-patterns'
import { OrganizationInviteError } from './OrganizationInviteError'
+import {
+ InterstitialAccountRow,
+ InterstitialLayout,
+ SupabaseLogo,
+} from '@/components/layouts/InterstitialLayout'
import { useOrganizationAcceptInvitationMutation } from '@/data/organization-members/organization-invitation-accept-mutation'
import { useOrganizationInvitationTokenQuery } from '@/data/organization-members/organization-invitation-token-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
-import { useProfile } from '@/lib/profile'
+import { useProfile, useProfileNameAndPicture } from '@/lib/profile'
export const OrganizationInvite = () => {
const router = useRouter()
const isLoggedIn = useIsLoggedIn()
const { profile, isLoading: isLoadingProfile } = useProfile()
+ const { username, avatarUrl, primaryEmail } = useProfileNameAndPicture()
const { slug, token } = useParams()
const isSignUpEnabled = useIsFeatureEnabled('dashboard_auth:sign_up')
@@ -31,18 +37,53 @@ export const OrganizationInvite = () => {
{
retry: false,
refetchOnWindowFocus: false,
- enabled: !!profile,
+ 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 inviteIsNoLongerValid =
- error?.code === 401 && error?.message.includes('Failed to retrieve organization')
- const organizationName = isSuccessInvitation ? data?.organization_name : 'An organization'
+ 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 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({
@@ -60,92 +101,107 @@ export const OrganizationInvite = () => {
joinOrganization({ slug, token })
}
- return (
-
You have been invited to join
-{organizationName}
- {isSuccessInvitation && ( -{`Organization slug: ${slug}`}
- )} -- Your email address {profile?.primary_email} does not match the email address this - invitation was sent to. -
-- To accept this invitation, you will need to{' '} - { - await signOut() - router.reload() - }} - > - sign out - {' '} - and then sign in or create a new account using the same email address used in the - invitation. -
-The invite token has expired.
-- Please request a new one from the organization owner. -
-The invite token is invalid.
-- You could be logged in with the wrong account. Try copying and pasting the link from the - invite email, or ask the organization owner to invite you again. -
-Signed in as
++ {displayName || Loading account} +
+{field.comment}
} > } - onChange={(value: any) => onUpdateField({ [field.name]: value })} + onChange={(value: string | null) => onUpdateField({ [field.name]: value })} disabled={!isEditable} /> ) From 52237e1ae481066fed60a899357a911658b3b95f Mon Sep 17 00:00:00 2001 From: Joshen LimIf you need to go beyond the included quota, simply switch off your spend cap diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx index 2c52ed8cc0b..2b8c6a68239 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx @@ -151,8 +151,8 @@ export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalP
+ Please try refreshing your browser, or contact support if the issue persists +
+Error: {(error as any)?.message ?? 'Unknown'}
+ > + } + actions={ , - ]} - > -- Please try refreshing your browser, or contact support if the issue persists -
-Error: {(error as any)?.message ?? 'Unknown'}
-Anyone can edit or delete data in this table.
-+ {p.description} +
+- {p.description} -
-An edge function with this name already exists.
+ + +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{' '}
+