refactor(stripe projects): always create a new org (#44634)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor based on https://github.com/supabase/platform/pull/31325

## What is the current behavior?

We presented a page to Stripe users to let them either pick an existing
org or create one.

## What is the new behavior?

We're forcing them to create a new one (or show that there was one
already linked).

- It also adds the option to sign out when there's a conflict. Fixes
https://linear.app/supabase/issue/API-963/add-a-button-to-logout-from-the-page-you-must-be-logged-in-as-x-to
- And adds the link to root from the logo.


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

* **New Features**
  * Added organization preview creation endpoint for billing workflows.

* **Bug Fixes**
* Removed organization-picking flow from Stripe Projects login; users
now proceed directly with confirmation.
  * Added a "Sign out" button on error pages.

* **Refactor**
  * Removed a legacy billing partner option.
  * Made the Supabase logo clickable for quick navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Raúl BarrosoandIvan Vasilov authored and GitHub committed 2026-04-08 17:11:28 +00:00
1 parent fe67f11190
commit 1797842ee5
4 files changed
+40 -194

No files matched your search

@@ -1,5 +1,6 @@
import { useTheme } from 'next-themes'
import Image from 'next/legacy/image'
import Link from 'next/link'
import type { PropsWithChildren } from 'react'
import { Separator } from 'ui'
@@ -29,17 +30,19 @@ export const APIAuthorizationLayout = ({
<div className="mx-auto px-4 sm:px-6">
<div className="max-w-xl flex justify-between items-center mx-auto py-4">
<div className="flex justify-start lg:w-0 lg:flex-1 items-center">
<span className="sr-only">Supabase</span>
<Image
src={
resolvedTheme?.includes('dark')
? `${BASE_PATH}/img/supabase-dark.svg`
: `${BASE_PATH}/img/supabase-light.svg`
}
alt="Supabase Logo"
height={20}
width={105}
/>
<Link href="/">
<span className="sr-only">Supabase</span>
<Image
src={
resolvedTheme?.includes('dark')
? `${BASE_PATH}/img/supabase-dark.svg`
: `${BASE_PATH}/img/supabase-light.svg`
}
alt="Supabase Logo"
height={20}
width={105}
/>
</Link>
</div>
</div>
</div>
@@ -6,22 +6,16 @@ import type { ResponseError, UseCustomMutationOptions } from '@/types'
type ConfirmAccountRequestVariables = {
arId: string
organizationId?: number
organizationName?: string
}
async function confirmAccountRequest({
arId,
organizationId,
organizationName,
}: ConfirmAccountRequestVariables) {
async function confirmAccountRequest({ arId }: ConfirmAccountRequestVariables) {
if (!arId) throw new Error('Account request ID is required')
const { data, error } = await post(
'/platform/stripe/projects/provisioning/account_requests/{id}/confirm',
{
params: { path: { id: arId } },
body: { organization_id: organizationId, organization_name: organizationName },
body: {},
}
)
@@ -2,41 +2,28 @@ import { useQuery } from '@tanstack/react-query'
import { useParams } from 'common'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
Input_Shadcn_,
LogoLoader,
WarningIcon,
} from 'ui'
import { APIAuthorizationLayout } from '@/components/layouts/APIAuthorizationLayout'
import { OrganizationSelector } from '@/components/ui/org-selector'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useConfirmAccountRequestMutation } from '@/data/partners/stripe-projects-confirm-mutation'
import {
accountRequestQueryOptions,
type AccountRequestDetails,
} from '@/data/partners/stripe-projects-query'
import { accountRequestQueryOptions } from '@/data/partners/stripe-projects-query'
import { withAuth } from '@/hooks/misc/withAuth'
import { useSignOut } from '@/lib/auth'
type OrgSummary = NonNullable<AccountRequestDetails['linked_organization']>
const StripeProjectsLoginPage = () => {
const router = useRouter()
const { ar_id } = useParams()
const signOut = useSignOut()
const [selectedOrg, setSelectedOrg] = useState<OrgSummary | null>(null)
const [orgConfirmed, setOrgConfirmed] = useState(false)
const [creatingNewOrg, setCreatingNewOrg] = useState(false)
const [newOrgName, setNewOrgName] = useState('')
const {
data: accountRequest,
isPending,
@@ -44,10 +31,9 @@ const StripeProjectsLoginPage = () => {
isError,
error,
} = useQuery(accountRequestQueryOptions({ arId: ar_id }))
const { data: organizations = [] } = useOrganizationsQuery()
const {
mutateAsync: confirmAccountRequest,
mutate: confirmAccountRequest,
isPending: isConfirming,
isSuccess: isConfirmed,
} = useConfirmAccountRequestMutation()
@@ -61,38 +47,19 @@ const StripeProjectsLoginPage = () => {
}
}, [router.isReady, ar_id, router])
const handleApprove = async (organizationId?: number, organizationName?: string) => {
const handleApprove = async () => {
if (!ar_id || isConfirming) return
confirmAccountRequest({ arId: ar_id, organizationId, organizationName })
confirmAccountRequest({ arId: ar_id })
}
// linked_organization is set when an org is already linked to this Stripe account+org pair
// userOrgs is the list of user's orgs to pick from (only when no linked org)
const linkedOrg = accountRequest?.linked_organization
const userOrgs = organizations ?? []
const emailMatches = accountRequest?.email_matches ?? false
const orgCount = userOrgs.length
// isReauth = org already linked, user is just completing the authorization flow again
const isReauth = !!linkedOrg
const isLinking = isReauth || orgCount >= 1
const loadingText = isReauth
? 'Completing authorization...'
: creatingNewOrg || orgCount === 0
? 'Creating your organization...'
: 'Linking your organization...'
const successTitle = isReauth
? 'Authorization Complete'
: creatingNewOrg || orgCount === 0
? 'Organization Created'
: 'Organization Linked'
const successDescription = isReauth
const loadingText = linkedOrg ? 'Completing authorization...' : 'Creating your organization...'
const successTitle = linkedOrg ? 'Authorization Complete' : 'Organization Created'
const successDescription = linkedOrg
? null
: creatingNewOrg || orgCount === 0
? 'Your Supabase organization has been created and linked to your Stripe account.'
: 'Your Supabase organization has been linked to your Stripe account.'
: 'Your Supabase organization has been created and linked to your Stripe account.'
return (
<APIAuthorizationLayout HeadProvider={Head}>
@@ -135,7 +102,7 @@ const StripeProjectsLoginPage = () => {
</div>
</>
) : linkedOrg ? (
// Org already linked to this Stripe account — inform user, no choice
// Org already linked to this Stripe account — inform user and confirm
<>
<p className="mt-4 text-sm text-foreground-light text-center">
Your organization <strong>{linkedOrg.name}</strong> is already linked to your
@@ -146,14 +113,14 @@ const StripeProjectsLoginPage = () => {
size="large"
type="primary"
disabled={isConfirming}
onClick={() => handleApprove()}
onClick={handleApprove}
>
Continue
</Button>
</div>
</>
) : orgCount === 0 ? (
// No orgs at all — a new one will be created
) : (
// No linked org — a new one will be created
<>
<p className="mt-4 text-sm text-foreground-light text-center">
A new Supabase organization will be created and linked to your Stripe account.
@@ -163,133 +130,23 @@ const StripeProjectsLoginPage = () => {
size="large"
type="primary"
disabled={isConfirming}
onClick={() => handleApprove()}
onClick={handleApprove}
>
Approve
</Button>
</div>
</>
) : creatingNewOrg ? (
// User chose to create a new org — show name input
<>
<p className="mt-4 text-sm text-foreground-light text-center">
A new <strong>Free</strong> organization will be created and linked to your Stripe
account for provisioning Supabase resources.
</p>
<div className="mt-4 w-80 flex flex-col gap-3">
<Input_Shadcn_
type="text"
placeholder="Organization name"
value={newOrgName}
onChange={(e) => setNewOrgName(e.target.value)}
maxLength={64}
autoFocus
/>
</div>
<div className="py-6 flex flex-col items-center gap-3">
<Button
size="large"
type="primary"
disabled={isConfirming || newOrgName.trim().length === 0}
onClick={() => handleApprove(undefined, newOrgName.trim())}
>
Create and Approve
</Button>
<button
className="text-sm text-foreground-lighter underline hover:text-foreground-light"
onClick={() => {
setCreatingNewOrg(false)
setNewOrgName('')
}}
>
Back
</button>
</div>
</>
) : orgCount === 1 ? (
// Exactly one org — show its name and option to create new
<>
<p className="mt-4 text-sm text-foreground-light text-center">
Your organization <strong>{userOrgs[0].name}</strong> will be linked to your
Stripe account. Supabase resources from Stripe will be provisioned into this
organization.
</p>
<div className="py-6 flex flex-col items-center gap-3">
<Button
size="large"
type="primary"
disabled={isConfirming}
onClick={() => handleApprove(userOrgs[0].id)}
>
Approve
</Button>
<button
className="text-sm text-foreground-lighter underline hover:text-foreground-light"
onClick={() => setCreatingNewOrg(true)}
>
or create a new free organization
</button>
</div>
</>
) : !orgConfirmed ? (
// 2+ orgs — show picker
<>
<p className="mt-4 text-sm text-foreground-light text-center">
Select the organization you'd like to link to your Stripe account. Supabase
resources from Stripe will be provisioned into this organization.
</p>
<div className="mt-4 w-96">
<OrganizationSelector
onSelect={(slug) => {
const org = userOrgs.find((o) => o.slug === slug) ?? null
setSelectedOrg(org)
if (org) {
setOrgConfirmed(true)
}
}}
maxOrgsToShow={3}
canCreateNewOrg={false}
/>
<button
className="mt-3 w-full text-sm text-foreground-lighter underline hover:text-foreground-light"
onClick={() => setCreatingNewOrg(true)}
>
or create a new free organization
</button>
</div>
</>
) : (
// 2+ orgs — org selected, show confirmation
<>
<p className="mt-4 text-sm text-foreground-light text-center">
Link <strong>{selectedOrg!.name}</strong> to your Stripe account?
</p>
<div className="py-6 flex flex-col items-center gap-3">
<Button
size="large"
type="primary"
disabled={isConfirming}
onClick={() => handleApprove(selectedOrg!.id)}
>
Approve
</Button>
<button
className="text-sm text-foreground-lighter underline hover:text-foreground-light"
onClick={() => {
setSelectedOrg(null)
setOrgConfirmed(false)
}}
>
Change organization
</button>
</div>
</>
)}
</>
) : isError ? (
<>
<h2 className="py-2 text-lg font-medium text-destructive">Error</h2>
<p className="text-foreground-light">{error?.message}</p>
<div className="py-6">
<Button size="large" type="default" onClick={() => signOut()}>
Sign out
</Button>
</div>
</>
) : null}
</div>
+5 -13
View File
@@ -4764,10 +4764,7 @@ export interface components {
payment_intent_id: string
size?: string
}
ConfirmRequestDto: {
organization_id?: number
organization_name?: string
}
ConfirmRequestDto: Record<string, never>
ConfirmResponseDto: {
organization_slug: string
success: boolean
@@ -5139,12 +5136,7 @@ export interface components {
| {
billing_email: string | null
/** @enum {string|null} */
billing_partner:
| 'fly'
| 'aws_marketplace'
| 'vercel_marketplace'
| 'stripe_projects'
| null
billing_partner: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | null
id: number
is_owner: boolean
name: string
@@ -6636,7 +6628,7 @@ export interface components {
}[]
billing_cycle_anchor: number
/** @enum {string} */
billing_partner?: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | 'stripe_projects'
billing_partner?: 'fly' | 'aws_marketplace' | 'vercel_marketplace'
billing_via_partner: boolean
current_period_end: number
current_period_start: number
@@ -7683,7 +7675,7 @@ export interface components {
OrganizationResponse: {
billing_email: string | null
/** @enum {string|null} */
billing_partner: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | 'stripe_projects' | null
billing_partner: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | null
id: number
is_owner: boolean
name: string
@@ -7753,7 +7745,7 @@ export interface components {
OrganizationSlugResponse: {
billing_email: string | null
/** @enum {string|null} */
billing_partner: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | 'stripe_projects' | null
billing_partner: 'fly' | 'aws_marketplace' | 'vercel_marketplace' | null
has_oriole_project: boolean
id: number
name: string