mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user