Files
supabase/apps/studio/components/layouts/InterstitialLayout.tsx
Danny White fd01c665e2 feat(studio): move Stripe Projects to connect interstitial (#45862)
## 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?

Feature update. Resolves DEPR-553.

## What is the current behavior?

Stripe Projects login still uses the older `APIAuthorizationLayout`
surface, so it does not match the newer shared connect interstitial
pattern used by organisation invites and CLI login.

## What is the new behavior?

Moves `/partners/stripe/projects/login` onto the shared
`InterstitialLayout` while preserving the existing `ar_id` account
request lookup, confirmation mutation, wrong-account sign-out path, and
missing-parameter redirect.

The temporary reviewer mocks have been removed after approval.

## Testing instructions

Automated checks run locally:

- `pnpm --dir apps/studio exec prettier --write
pages/partners/stripe/projects/login.tsx
components/layouts/InterstitialLayout.tsx`
- `pnpm --dir apps/studio exec eslint
pages/partners/stripe/projects/login.tsx
components/layouts/InterstitialLayout.tsx`
- `git diff --check`

`pnpm --dir apps/studio exec tsc --noEmit` was also run earlier on this
branch, but still fails on existing unrelated issues in
`components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx`
and `packages/common/marketplace-client.ts`.

Manual Stripe Projects testing requires a real account request. Opening
`/partners/stripe/projects/login` without an `ar_id` redirects to `/404`
by design.

If you need the real flow:

1. Use the Stripe staging provider. In the Stripe CLI flow, run `export
DEV_MODE=true` so the provider is `Supabase_Staging_Env`.
2. From a local project directory, run `stripe projects init` and
complete the Stripe setup flow.
3. Run `stripe projects add Supabase_Staging_Env`.
4. When the browser opens the Supabase authorization URL, keep the
generated path and query string exactly as-is, including `ar_id`, but
replace only the origin with this PR preview deployment origin.

Note: the staging Stripe Projects flow can still incur real Stripe
costs; use the staging provider and coordinate refunds with team billing
if needed.

## Additional context

This is a deliberately small stacked slice toward the broader shared
connect interstitial work.


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

## Summary by CodeRabbit

* **User Interface**
* Redesigned Stripe authorization login page with improved layout and
visual state management
* Enhanced account row component to support flexible action buttons and
styling
* Added clearer messaging and UI states for authorization scenarios
(pending, success, errors, and account mismatches)

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45862)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-14 12:13:10 +10:00

145 lines
4.2 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
containerClassName?: string
cardClassName?: string
titleClassName?: string
descriptionClassName?: string
}
export const InterstitialLayout = ({
logo,
title,
description,
containerClassName,
cardClassName,
titleClassName,
descriptionClassName,
children,
}: PropsWithChildren<InterstitialLayoutProps>) => {
const TitleElement = typeof title === 'string' ? 'h1' : 'div'
const DescriptionElement = typeof description === 'string' ? 'p' : 'div'
return (
<div
className={cn(
'flex min-h-screen w-full items-center justify-center bg-studio px-2 py-6',
containerClassName
)}
>
<MotionCard
layout="size"
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className={cn('mx-auto w-full max-w-[400px] overflow-hidden', cardClassName)}
>
{(logo || title || description) && (
<CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center font-normal [--card-padding-x:1.5rem]">
{logo && <div className="mb-4 flex justify-center">{logo}</div>}
{(title || description) && (
<div className="flex flex-col items-center gap-1">
{title && (
<TitleElement
className={cn(
'font-sans text-lg font-medium tracking-tight text-balance text-foreground',
titleClassName
)}
>
{title}
</TitleElement>
)}
{description && (
<DescriptionElement
className={cn(
'!m-0 px-3 text-sm leading-tight !text-balance text-foreground-lighter',
descriptionClassName
)}
>
{description}
</DescriptionElement>
)}
</div>
)}
</CardHeader>
)}
{children}
</MotionCard>
</div>
)
}
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>
)
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>
)
export const SupabaseLogo = () => (
<LogoBox>
<img alt="Supabase" src={`${BASE_PATH}/img/supabase-logo.svg`} className="size-7" />
</LogoBox>
)
export const PartnerLogo = ({ src, alt }: { src: string; alt: string }) => (
<LogoBox>
<img alt={alt} src={src} className="size-full object-cover" />
</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>
)