mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): add return to Vercel escape hatch (#48311)
## 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? Bug fix / UX improvement for the Vercel Deploy Button create-project interstitial. ## What is the current behavior? On the Vercel create-project step, the organization picker is locked (correct — the integration is bound to that org) and Cancel is hidden. If the org can't create a free project (member free-project limits), users hit a dead end: Upgrade may not help, and there's no way out of the popup. Also includes a small capitalisation nit on the Vercel install page. | Before | | --- | | <img width="800" height="629" alt="Create Vercel Project Supabase" src="https://github.com/user-attachments/assets/2acdc7a3-eb99-43c6-9135-557370647da1" /> | ## What is the new behavior? - Replaces `hideCancelButton` with `cancelAction: 'studio' | 'vercel' | 'hidden'` - Vercel create flow shows **Return to Vercel**, which redirects to the install `next` URL (closing the popup cleanly) - Free-project-limit admonition adds a Vercel-only hint pointing at that button: “Or return to Vercel and restart with a different organization.” - Main `/new` Cancel behaviour is unchanged - Org picker stays disabled ## Additional context Org switching mid-create is intentionally not allowed. That would orphan the Vercel install. Returning to Vercel is the safe escape hatch so users can restart Deploy Button with another org, or free a project slot / upgrade and try again. ## To test As far as I can tell, this is impossible to test on prod. Shortly after merge though, you could test the following: - [ ] Happy path: create still works; Return to Vercel is secondary and does not block submit - [ ] Free-limit blocked org: Create disabled, Return to Vercel visible and redirects to `next` - [ ] Main `/new`: Cancel still goes to last org / organizations <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced project creation flow for Vercel: when a valid return destination is available, users can choose **“Return to Vercel”**. - Added additional messaging in the free-project-limit warning to guide users back to Vercel and restart with a different organization (when applicable). - **Bug Fixes** - Improved cancel behavior and routing consistency by only enabling Vercel return when the destination is valid. - **Style** - Updated the Vercel integration interstitial title capitalization for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
7838676902
commit
c9ed51c99e
6 files changed
+82
-15
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import {
|
||||
getValidVercelReturnUrl,
|
||||
isVercelUrl,
|
||||
} from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
|
||||
|
||||
describe('isVercelUrl', () => {
|
||||
test('accepts https vercel.com urls', () => {
|
||||
expect(isVercelUrl('https://vercel.com/callback')).toBe(true)
|
||||
})
|
||||
|
||||
test('rejects non-vercel and invalid urls', () => {
|
||||
expect(isVercelUrl('https://example.com')).toBe(false)
|
||||
expect(isVercelUrl('http://vercel.com')).toBe(false)
|
||||
expect(isVercelUrl('not-a-url')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getValidVercelReturnUrl', () => {
|
||||
test('returns the url when it is a valid vercel return url', () => {
|
||||
expect(getValidVercelReturnUrl('https://vercel.com/callback')).toBe(
|
||||
'https://vercel.com/callback'
|
||||
)
|
||||
})
|
||||
|
||||
test('returns undefined for missing or invalid next values', () => {
|
||||
expect(getValidVercelReturnUrl(undefined)).toBeUndefined()
|
||||
expect(getValidVercelReturnUrl('https://example.com')).toBeUndefined()
|
||||
expect(getValidVercelReturnUrl('not-a-url')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,12 @@ export function isVercelUrl(url: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `next` when it is a safe Vercel return URL; otherwise undefined. */
|
||||
export function getValidVercelReturnUrl(next: string | undefined): string | undefined {
|
||||
if (typeof next === 'string' && isVercelUrl(next)) return next
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function findVercelIntegrationByConfigurationId(
|
||||
integrations: Integration[] | undefined,
|
||||
configurationId: string | undefined
|
||||
|
||||
@@ -6,9 +6,13 @@ import type { MemberWithFreeProjectLimit } from '@/data/organizations/free-proje
|
||||
|
||||
interface FreeProjectLimitWarningProps {
|
||||
membersExceededLimit: MemberWithFreeProjectLimit[]
|
||||
showVercelReturnHint?: boolean
|
||||
}
|
||||
|
||||
export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLimitWarningProps) => {
|
||||
export const FreeProjectLimitWarning = ({
|
||||
membersExceededLimit,
|
||||
showVercelReturnHint = false,
|
||||
}: FreeProjectLimitWarningProps) => {
|
||||
return (
|
||||
<Panel.Content>
|
||||
<Admonition
|
||||
@@ -32,6 +36,11 @@ export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLim
|
||||
These members will need to either delete, pause, or upgrade one or more of these
|
||||
projects before you're able to create a free project within this organization.
|
||||
</p>
|
||||
{showVercelReturnHint && (
|
||||
<p className="text-sm leading-normal">
|
||||
Or return to Vercel and restart with a different organization.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<UpgradePlanButton
|
||||
source="freeProjectLimitWarning"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useFlag } from 'common'
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
import {
|
||||
@@ -16,12 +16,15 @@ import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
|
||||
import { CreateProjectForm } from './ProjectCreation.schema'
|
||||
import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils'
|
||||
import { getValidVercelReturnUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { OrgProject } from '@/data/projects/org-projects-infinite-query'
|
||||
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export type ProjectCreationCancelAction = 'studio' | 'vercel' | 'hidden'
|
||||
|
||||
interface ProjectCreationFooterProps {
|
||||
form: UseFormReturn<CreateProjectForm>
|
||||
canCreateProject: boolean
|
||||
@@ -29,7 +32,7 @@ interface ProjectCreationFooterProps {
|
||||
organizationProjects: OrgProject[]
|
||||
isCreatingNewProject: boolean
|
||||
isSuccessNewProject: boolean
|
||||
hideCancelButton: boolean
|
||||
cancelAction?: ProjectCreationCancelAction
|
||||
}
|
||||
|
||||
export const ProjectCreationFooter = ({
|
||||
@@ -39,9 +42,10 @@ export const ProjectCreationFooter = ({
|
||||
organizationProjects,
|
||||
isCreatingNewProject,
|
||||
isSuccessNewProject,
|
||||
hideCancelButton,
|
||||
cancelAction = 'studio',
|
||||
}: ProjectCreationFooterProps) => {
|
||||
const router = useRouter()
|
||||
const { next } = useParams()
|
||||
const { data: currentOrg } = useSelectedOrganizationQuery()
|
||||
const isFreePlan = currentOrg?.plan?.id === 'free'
|
||||
const { lastVisitedOrganization } = useLastVisitedOrganization()
|
||||
@@ -53,6 +57,9 @@ export const ProjectCreationFooter = ({
|
||||
? 0
|
||||
: monthlyInstancePrice(instanceSize) - availableComputeCredits
|
||||
|
||||
const vercelReturnUrl = getValidVercelReturnUrl(next)
|
||||
const canReturnToVercel = cancelAction === 'vercel' && vercelReturnUrl !== undefined
|
||||
|
||||
// [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
|
||||
const monthlyComputeCosts =
|
||||
// current project costs
|
||||
@@ -66,6 +73,17 @@ export const ProjectCreationFooter = ({
|
||||
// compute credits
|
||||
10
|
||||
|
||||
const onCancel = () => {
|
||||
if (canReturnToVercel && vercelReturnUrl) {
|
||||
window.location.href = vercelReturnUrl
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to Studio when cancelAction is studio, or when vercel next is missing/invalid
|
||||
if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
|
||||
else router.push('/organizations')
|
||||
}
|
||||
|
||||
return (
|
||||
<div key="panel-footer" className="grid grid-cols-12 w-full gap-4 items-center">
|
||||
<div className="col-span-4">
|
||||
@@ -169,16 +187,13 @@ export const ProjectCreationFooter = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-end col-span-8 space-x-2 ml-auto">
|
||||
{!hideCancelButton && (
|
||||
{cancelAction !== 'hidden' && (
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={isCreatingNewProject || isSuccessNewProject}
|
||||
onClick={() => {
|
||||
if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
|
||||
else router.push('/organizations')
|
||||
}}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{canReturnToVercel ? 'Return to Vercel' : 'Cancel'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -35,6 +35,7 @@ import { ProjectNameInput } from './ProjectNameInput'
|
||||
import { RegionSelector } from './RegionSelector'
|
||||
import { SecurityOptions } from './SecurityOptions'
|
||||
import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
|
||||
import { getValidVercelReturnUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
|
||||
import {
|
||||
GitHubRepositoryField,
|
||||
useGitHubRepositoryOptions,
|
||||
@@ -90,7 +91,7 @@ interface ProjectCreationFormProps {
|
||||
* - "Internal configuration" section
|
||||
* - "GitHub repository" field
|
||||
* - "Free project info" at the bottom
|
||||
* - "Cancel" button
|
||||
* - Shows Cancel as "Return to Vercel" (via `next`) instead of navigating into Studio
|
||||
* - Shows the following:
|
||||
* - "Data seeding" section
|
||||
* - When embedded in the Vercel interstitial, flattens Panel chrome so the shared
|
||||
@@ -105,7 +106,8 @@ export const ProjectCreationForm = ({
|
||||
const track = useTrack()
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const { slug, projectName, externalId } = useParams()
|
||||
const { slug, projectName, externalId, next } = useParams()
|
||||
const canReturnToVercel = getValidVercelReturnUrl(next) !== undefined
|
||||
const trackFunnelError = useTrackFunnelError()
|
||||
const defaultProvider = useDefaultProvider()
|
||||
|
||||
@@ -607,7 +609,7 @@ export const ProjectCreationForm = ({
|
||||
organizationProjects={organizationProjects}
|
||||
isCreatingNewProject={isCreatingNewProject}
|
||||
isSuccessNewProject={isSuccessNewProject}
|
||||
hideCancelButton={isVercelIntegrationFlow}
|
||||
cancelAction={isVercelIntegrationFlow ? 'vercel' : 'studio'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -700,7 +702,10 @@ export const ProjectCreationForm = ({
|
||||
{freePlanWithExceedingLimits ? (
|
||||
isAdmin &&
|
||||
slug && (
|
||||
<FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
|
||||
<FreeProjectLimitWarning
|
||||
membersExceededLimit={membersExceededLimit || []}
|
||||
showVercelReturnHint={isVercelIntegrationFlow && canReturnToVercel}
|
||||
/>
|
||||
)
|
||||
) : hasOutstandingInvoices ? (
|
||||
<Panel.Content>
|
||||
|
||||
@@ -250,7 +250,7 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
|
||||
<InterstitialLayout
|
||||
logo={<VercelIntegrationLogo />}
|
||||
title="Install Vercel Integration"
|
||||
title="Install Vercel integration"
|
||||
description="Choose the Supabase organization Vercel can connect to"
|
||||
footer={<VercelIntegrationFooter />}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user