diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts new file mode 100644 index 00000000000..00cd23ecbf7 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts @@ -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() + }) +}) diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts index a6802341ab7..05db949804d 100644 --- a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts @@ -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 diff --git a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx index e3b8389232c..ba554365f5a 100644 --- a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx @@ -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 ( + {showVercelReturnHint && ( +

+ Or return to Vercel and restart with a different organization. +

+ )} 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 (
@@ -169,16 +187,13 @@ export const ProjectCreationFooter = ({
- {!hideCancelButton && ( + {cancelAction !== 'hidden' && ( )}