mirror of
https://github.com/supabase/supabase.git
synced 2026-06-13 10:09:12 +08:00
> [!CAUTION] > The `do-not-merge` label has been applied because this contains mocks for easier review and testing. I'll remove those mocks before merging. ## What kind of change does this PR introduce? Feature. Part of the shared Connect UI (interstitial) rollout. Previous slices: #46058, #45909, #45862. ## What is the current behavior? The `/authorize` MCP/OAuth consent screen uses the old `Card`/`Alert` layout. ## What is the new behavior? - Wraps all `/authorize` states in `InterstitialLayout` (the shared full-screen centered card used across Connect flows) - Shows a quiet footnote below the Cancel button ("Authorizing will redirect you to \<url\>") for non-localhost redirect URIs, so users can verify the destination before approving. No extra friction for localhost flows (local MCP servers) | Before | After | | --- | --- | | <img width="692" height="997" alt="Authorize API access Supabase-F6C3747A-5077-43D8-A509-3E16B1DDC168" src="https://github.com/user-attachments/assets/e86dde34-94cb-48ef-b026-66aac9122df6" /> | <img width="692" height="997" alt="Authorize API Access Supabase-FE6FD8B3-1159-4EA5-94D7-EA5CEA7A25F3" src="https://github.com/user-attachments/assets/c1a94a44-51d9-40d8-8046-f3104a27b929" /> | | <img width="692" height="997" alt="Authorize API access Supabase-86742351-3521-4B62-AF87-403CB7E7F4F5" src="https://github.com/user-attachments/assets/41cff7af-b9e4-4a20-a979-7148b4220265" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-B665B4A4-600F-462B-8C97-84B171EC3103" src="https://github.com/user-attachments/assets/804286f2-ce51-45ab-bb3f-315f8ac62445" /> | | <img width="692" height="997" alt="Authorize API access Supabase-C73DC3D0-8646-4E6E-A259-3E84AE46DAF2" src="https://github.com/user-attachments/assets/8f285edb-438f-4262-9faa-f1133c679ed4" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-FEA86625-27D5-4DB5-B4D4-1A2CB804E56E" src="https://github.com/user-attachments/assets/b54f2ceb-e1cf-4c7e-be3f-8e1b0942e9a4" /> | | <img width="692" height="997" alt="Authorize API access Supabase-48E0C7CB-DDDD-4305-B821-F3BEB52C4A4E" src="https://github.com/user-attachments/assets/7d123c57-e05d-408c-8df9-d747a3afd714" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-CE8F9905-FAE0-4C06-B77A-9F269B2100FE" src="https://github.com/user-attachments/assets/9f403b83-5de3-43c8-a592-c3022e041243" /> | | <img width="692" height="997" alt="Authorize API access Supabase-E37D2CD5-476F-4F49-A5FB-631B265025DC" src="https://github.com/user-attachments/assets/3d235315-d7c0-4279-b23f-e8b595888511" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-DF078AEB-BB78-4647-9FA2-5D5403CCA5D6" src="https://github.com/user-attachments/assets/53d51718-8707-4b97-9cbe-8e523f4ce0e0" /> | | <img width="692" height="997" alt="Authorize API access Supabase-D6F6817F-D8DD-4D55-85BB-A15100814AAB" src="https://github.com/user-attachments/assets/c80c5579-772a-4dfe-a247-b0b9772b9690" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-E457B580-9786-43AD-9CF9-FE4F5BB8E785" src="https://github.com/user-attachments/assets/30c47b05-edf5-4380-a2f1-aedb99482540" /> | | <img width="692" height="997" alt="Authorize API access Supabase-4F3D6AA4-E2E3-4526-B391-49B6E0861911" src="https://github.com/user-attachments/assets/ffbe5b65-6eef-49d7-95f1-c29072c320b8" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-CA9FFCC9-4CA2-4718-AD49-B02D86C6EF6A" src="https://github.com/user-attachments/assets/8fd7ff39-19f5-4414-af13-3821290735b2" /> | | <img width="692" height="997" alt="Authorize API access Supabase-E507B7A5-9AD0-4F17-8743-63A7B47D171A" src="https://github.com/user-attachments/assets/1639b5cc-69c4-4a43-b049-6f989e2cdbb1" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-9844BB27-2429-4BA6-BD36-1AB54099F44F" src="https://github.com/user-attachments/assets/a94b88e2-9c2f-4941-840a-5182342bb335" /> | | <img width="692" height="997" alt="Authorize API access Supabase-27684173-9DBB-4F6E-9F7F-87EFD4E10A5F" src="https://github.com/user-attachments/assets/91794c96-8a81-4d83-9c97-01d134639676" /> | <img width="692" height="997" alt="Authorize Cursor Supabase-04E31F7B-D098-4814-A394-01CE3D3E5A51" src="https://github.com/user-attachments/assets/ba0284a3-363c-4aa5-9e4a-c378aed9c42c" /> | | <img width="692" height="997" alt="Authorize API access Supabase-207CBC69-4957-499C-92E8-163F2B34C8AD" src="https://github.com/user-attachments/assets/1bafedd2-bba8-473c-ba57-637289f1c940" /> | <img width="692" height="997" alt="Authorize API Access Supabase-C1627071-4AE2-4012-8F7C-4E6D883618A3" src="https://github.com/user-attachments/assets/a6fc6125-3c1e-4b8c-821a-c3c9f32f3cc0" /> | ## To test A mock toolbar is included for easy local testing. Navigate to `/authorize?mock=loading` and then switch between the following variants: | State | What to check | | --- | --- | | `loading` | Shimmer skeleton inside the card | | `ready` | Regular waiting state | | `approving` | Authorize button shows spinner, both buttons disabled | | `approved` | Success admonition: "Authorization approved" | | `expired` | Warning admonition: "Authorization request expired", no action buttons | | `organizations-loading` | Org selector shimmer, no action buttons | | `organizations-error` | "Unable to load organizations" admonition, no action buttons | | `empty` | "No organizations found" admonition, no action buttons | | `not-member` | "Organization unavailable" admonition, no action buttons | | `error` | "Unable to load authorization" error screen | Then please test the `organization_slug` prefill: `/authorize?mock=ready&organization_slug=<your-org-name-here>`. That org selector should be pre-selected and locked. To test against a real OAuth app, use a registered app on `supabase.green` — the mock states cover all edge cases but a live round-trip confirms the approve/decline API calls. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added mock preview functionality for testing API authorization and Connect flows * Introduced collapsible, grouped permissions view for OAuth authorization requests * **Refactor** * Redesigned API authorization screens with improved layout and messaging * Restructured permissions display for better organization and clarity * **Bug Fixes** * Fixed inline link underline decoration color * **Tests** * Updated authorization flow test assertions to match new UI behavior <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46359?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Ali Waseem <waseema393@gmail.com>
164 lines
4.7 KiB
TypeScript
164 lines
4.7 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
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
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
|
|
|
|
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('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>
|
|
</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. */
|
|
export const PartnerLogo = ({ src, alt }: { src: string; alt: string }) => (
|
|
<LogoBox>
|
|
<img alt={alt} src={src} className="size-full object-cover" />
|
|
</LogoBox>
|
|
)
|
|
|
|
/** Supabase symbol (not the wordmark) rendered inset inside a LogoBox. */
|
|
export const SupabaseLogo = () => (
|
|
<LogoBox>
|
|
<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>
|
|
)
|