Files
supabase/apps/studio/components/layouts/InterstitialLayout.tsx
Saxon FletcherandDanny White 09ea558d54 Vercel install layout (#47550)
Bring the Vercel install layout up to date with other connect screens.
Resolves DEPR-615.

| Before | After |
| --- | --- |
| <img width="1600" height="1200" alt="CleanShot 2026-07-03 at 10 51
59@2x"
src="https://github.com/user-attachments/assets/6a8c2910-8c45-4c7d-8d38-8b80c5cf4c83"
/> | <img width="1150" height="1318" alt="CleanShot 2026-07-03 at 11 56
17@2x"
src="https://github.com/user-attachments/assets/3a5add5c-fea4-44f3-a368-5732257b27d9"
/> |

## Testing

- Open the deploy preview or staging URL for
`/dashboard/integrations/vercel/install` with callback params from a
real Vercel Marketplace install redirect (see _Vercel_ subheading
below).
- Confirm the install screen renders with the selected Supabase account,
organization picker, and primary install CTA.
- Remove required callback params such as `code`, `configurationId`, or
`source` to verify the "Missing Vercel installation details" warning
state.
- Clicking "Install integration" with an expired or reused `code` can
show `Creating Vercel integration failed: Failed to get Vercel access
token`; that is expected for preview UI validation. A full successful
install requires a fresh Vercel-generated code from the install flow.

To reiterate; this won’t work because the code will be invalid. But it
should show that everything is hooked up right for prod.

### Vercel

1. Go to Supabase. Open either org-level or project (settings) level
integrations.
2. Tap "[Install Vercel
Integration](https://vercel.com/integrations/supabase-local)".
3. Follow the install instructions from that Vercel page. This should
open a browser window.
4. Copy the URL of that browser window. Take the params and paste them
instead at the end of the deploy preview URL.

Example of #4:

```txt
Before:
https://supabase.com/dashboard/integrations/vercel/install?code=jDhIBDlD58zzLVtuSNjJpUSu&configurationId=icfg_v3dKllQIniSOwdVI3gypnZh3&next=https%3A%2F%2Fvercel.com%2Ftest-5706s-projects%2F~%2Fintegrations%2Ficfg_v3dKllQIniSOwdVI3gypnZh3%2Finstalled&source=marketplace&teamId=team_mPkGQZjTLBEUXh15b03iVsTg

After
https://studio-staging-git-chore-install-layout-supabase.vercel.app/dashboard/integrations/vercel/install?code=jDhIBDlD58zzLVtuSNjJpUSu&configurationId=icfg_v3dKllQIniSOwdVI3gypnZh3&next=https%3A%2F%2Fvercel.com%2Ftest-5706s-projects%2F~%2Fintegrations%2Ficfg_v3dKllQIniSOwdVI3gypnZh3%2Finstalled&source=marketplace&teamId=team_mPkGQZjTLBEUXh15b03iVsTg
```

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

## Summary by CodeRabbit

* **New Features**
* Updated the installation experience with a cleaner, more guided
layout.
* Added clearer organization selection during setup, including
visibility into which organizations are already installed.

* **Bug Fixes**
  * Improved loading and error handling during installation.
* Added clearer warnings for missing setup details, already-installed
integrations, and cases with no available organizations.
* Fixed routing behavior so organization selection is preserved more
reliably.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
2026-07-06 19:34:20 +10:00

198 lines
5.5 KiB
TypeScript

import { motion } from 'framer-motion'
import { ArrowRightLeft } from 'lucide-react'
import type { PropsWithChildren, ReactNode } from 'react'
import { Card, CardContent, CardHeader, cn } from 'ui'
import { ProfileImage } from '@/components/ui/ProfileImage'
import { BASE_PATH } from '@/lib/constants'
const MotionCard = motion.create(Card)
interface InterstitialLayoutProps {
logo?: ReactNode
title?: ReactNode
description?: ReactNode
/** Optional content rendered beneath the card (e.g. a terms disclaimer), at the card's width. */
footer?: ReactNode
containerClassName?: string
cardClassName?: string
titleClassName?: string
descriptionClassName?: string
}
/**
* Minimal full-screen centered layout for interstitial flows:
* partner authorization, org invites, CLI auth, credit redemption, etc.
*
* The logo, title, and description render inside the card (above children),
* so every consumer gets a consistent header for free.
*/
export const InterstitialLayout = ({
logo,
title,
description,
footer,
containerClassName,
cardClassName,
titleClassName,
descriptionClassName,
children,
}: PropsWithChildren<InterstitialLayoutProps>) => {
const TitleElement = typeof title === 'string' ? 'h1' : 'div'
const DescriptionElement = typeof description === 'string' ? 'p' : 'div'
const titleElement = title ? (
<TitleElement
className={cn(
'font-sans tracking-tight text-balance text-lg font-medium normal-case text-foreground',
titleClassName
)}
>
{title}
</TitleElement>
) : null
const descriptionElement = description ? (
<DescriptionElement
className={cn(
'!m-0 px-3 !text-balance text-sm text-foreground-lighter leading-tight',
descriptionClassName
)}
>
{description}
</DescriptionElement>
) : null
const card = (
<MotionCard
layout="size"
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className={cn('overflow-hidden max-w-[400px] w-full mx-auto', cardClassName)}
>
{(logo || title || description) && (
<CardHeader className="font-normal items-center gap-0 space-y-0 px-6 py-6 text-center [--card-padding-x:1.5rem] border-0">
{logo && <div className="mb-4 flex justify-center">{logo}</div>}
{(titleElement || descriptionElement) && (
<div className="flex flex-col items-center gap-1">
{titleElement}
{descriptionElement}
</div>
)}
</CardHeader>
)}
{children}
</MotionCard>
)
return (
<div
className={cn(
'flex min-h-screen w-full items-center justify-center bg-studio px-2 py-6',
containerClassName
)}
>
{footer ? (
<div className="flex w-full max-w-[400px] flex-col items-center gap-4">
{card}
<div className="px-2 text-center text-balance">{footer}</div>
</div>
) : (
card
)}
</div>
)
}
/**
* Standard rounded-rect logo container (48x48).
* Partner logos fill edge-to-edge (see `PartnerLogo`); the Supabase symbol and
* Lucide icons sit inset (sized at `size-7`).
*/
export const LogoBox = ({ children, className }: { children: ReactNode; className?: string }) => (
<div
className={cn(
'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted',
className
)}
>
{children}
</div>
)
/** Two pre-boxed logos side-by-side with a swap separator. */
export const LogoPair = ({ left, right }: { left: ReactNode; right: ReactNode }) => (
<div className="flex items-center justify-center gap-2.5">
{left}
<ArrowRightLeft className="size-4 text-foreground-muted" />
{right}
</div>
)
/** Partner logo rendered edge-to-edge inside a LogoBox by default. */
export const PartnerLogo = ({
src,
alt,
className,
imageClassName,
}: {
src: string
alt: string
className?: string
imageClassName?: string
}) => (
<LogoBox className={className}>
<img alt={alt} src={src} className={cn('size-full object-cover', imageClassName)} />
</LogoBox>
)
/**
* Sign-in destination mark, inset to match {@link SupabaseLogo}. Falls back to the destination's
* initial when no icon is available.
*/
export const DestinationLogo = ({ icon, name }: { icon?: ReactNode; name: string }) => (
<LogoBox>
{icon ?? <span className="text-lg font-medium text-foreground-light">{name.slice(0, 1)}</span>}
</LogoBox>
)
/** Supabase symbol (not the wordmark) rendered inset inside a LogoBox. */
export const SupabaseLogo = () => (
<LogoBox className="bg-surface-75">
<img alt="Supabase" src={`${BASE_PATH}/img/supabase-logo.svg`} className="size-7" />
</LogoBox>
)
export const InterstitialAccountRow = ({
avatarUrl,
displayName,
action,
className,
}: {
avatarUrl?: string
displayName?: string
action?: ReactNode
className?: string
}) => (
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50', className)}>
<CardContent
className={cn(
'flex gap-3 border-none',
action ? 'items-center px-4 py-3' : 'items-start p-3'
)}
>
<ProfileImage
src={avatarUrl}
alt={displayName}
className="size-8 flex-shrink-0 rounded-full border border-muted"
/>
<div className="min-w-0 flex-1">
<p className="text-xs text-foreground-light">Signed in as</p>
<p className="truncate text-sm text-foreground">
{displayName || <span className="invisible">Loading account</span>}
</p>
</div>
{action}
</CardContent>
</Card>
)