mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 05:09:12 +03:00
## What kind of change does this PR introduce? Feature + docs. Closes [DEPR-604](https://linear.app/supabase/issue/DEPR-604/define-connect-logo-asset-and-variant-contract). ## What is the current behavior? `/authorize` logo resolution trusted self-asserted requester `name` (and similar) for curated MCP marks, fell back to a letter tile when there was no usable icon, and always used theme-reactive tile chrome. This includes the scenario when pairing against unclassified uploaded OAuth app bitmaps. ## What is the new behavior? - [Documents the Connect logo asset/variant contract](https://design-system-git-danny-depr-604-connect-logo-contract-supabase.vercel.app/design-system/docs/ui-patterns/connect-interstitials#logos) (default to light, keep pairs matched, no theme-recolour of vendor SVGs). - Resolves curated partner logos from allowlisted `redirect_uri` hosts only (`claude.ai` / `anthropic.com`, `cursor.com` / `cursor.sh`, `chatgpt.com` / `openai.com`, `perplexity.ai`). - Unknown / missing / failed requester icons show `SupabaseLogo` alone (no letter tile). - Uploaded organisation OAuth app icons (unclassified bitmaps) pair with fixed light tile chrome (`border-black/10 bg-white` / `SupabaseLogo forceLight`) on both sides across Studio themes. - Curated partners keep theme-reactive tiles and may use dark assets when available. ### To test Real MCP clients (Claude, Cursor, etc.) only send users to **production** `/authorize`, so you cannot drive a local or preview Studio build from those tools. Use a Network override instead: 1. Start Studio and sign in (`pnpm dev:studio`, or use the Vercel preview once available). 2. Open `/dashboard/authorize?auth_id=foo` (any `auth_id` is fine — the real response may 404). 3. DevTools → **Network** → find `GET …/platform/oauth/authorizations/foo` (or whatever id you used). 4. Right-click → **Override content** (enable Local Overrides / pick a folder if prompted). 5. Paste one of the payloads below (status **200**), save, then reload the authorize page. 6. Keep `expires_at` in the future so the request does not look expired. The fields that matter for this PR are `name`, `icon`, and `redirect_uri`. #### Curated pair (allowlisted redirect) Expect Cursor mark + Supabase pair. Toggle light/dark: curated dark assets may swap; tiles stay theme-reactive (`bg-surface-75`). ```json { "name": "Cursor", "website": "https://cursor.com", "icon": null, "domain": "cursor.com", "redirect_uri": "https://cursor.com/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` #### Unknown → Supabase alone Expect Supabase bolt alone. No letter tile. No curated mark even if `name` says Claude. ```json { "name": "Acme", "website": "https://acme.example", "icon": null, "domain": "acme.example", "redirect_uri": "https://acme.example/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` #### Spoofed trusted name, non-allowlisted redirect (logo only) Expect Supabase alone (no Claude mark). This PR does **not** show the impersonation caution (that is coming in #48162). ```json { "name": "Claude", "website": "https://claude.ai", "icon": null, "domain": "claude.ai", "redirect_uri": "https://evil.com/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "dynamic" } ``` #### Uploaded OAuth app icon → forced-light pair Expect remote icon + Supabase pair with forced-light tiles (`border-black/10 bg-white`) on both sides in light and dark Studio themes. The icon URL below is the checked-in solid-colour Acme bitmap on this branch. ```json { "name": "Acme", "website": "https://acme.example", "icon": "https://raw.githubusercontent.com/supabase/supabase/danny/depr-604-connect-logo-contract/apps/design-system/public/img/icons/acme-oauth-icon.png", "domain": "acme.example", "redirect_uri": "https://acme.example/callback", "expires_at": "2099-01-01T00:00:00.000Z", "scopes": ["organizations:read", "projects:read"], "approved_at": null, "registration_type": "static" } ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved authorization interstitial branding with trusted requester logos and safer fallback behavior. * Added support for consistent light-theme treatment of uploaded OAuth app icons. * Added examples and documentation for unknown requesters, uploaded logos, and wrong-account states. * **Bug Fixes** * Prevented unverified or unavailable requester icons from being presented as trusted. * Ensured logo pairing remains visually consistent across light and dark themes. * **Tests** * Added coverage for trusted-host validation, fallback branding, icon loading failures, and theme behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
import { ArrowRightLeft, LogOut } from 'lucide-react'
|
|
import { Avatar, AvatarFallback, Button, Card, CardContent, CardHeader, cn } from 'ui'
|
|
|
|
export function LogoBox({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted',
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function LogoPair({ left, right }: { left: React.ReactNode; right: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center justify-center gap-2.5">
|
|
{left}
|
|
<ArrowRightLeft className="size-4 text-foreground-muted" />
|
|
{right}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function StripeLogo() {
|
|
return (
|
|
<LogoBox className="border-[#533afd] bg-[#533afd]">
|
|
<svg viewBox="0 0 512 512" className="size-full" aria-hidden>
|
|
<path
|
|
fill="#fff"
|
|
fillRule="evenodd"
|
|
d="m132 380 248-52.593V132l-248 53.208z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</LogoBox>
|
|
)
|
|
}
|
|
|
|
export function SupabaseLogo({ forceLight = false }: { forceLight?: boolean } = {}) {
|
|
return (
|
|
<LogoBox className={forceLight ? 'border-black/10 bg-white' : 'bg-surface-75'}>
|
|
<svg viewBox="0 0 109 113" className="size-7" aria-hidden>
|
|
<path
|
|
d="M63.708 110.284c-2.86 3.601-8.658 1.628-8.727-2.97L53.974 40.063h45.22c8.19 0 12.758 9.46 7.665 15.874L63.708 110.284Z"
|
|
fill="#3ECF8E"
|
|
/>
|
|
<path
|
|
d="M45.317 2.071c2.86-3.601 8.658-1.628 8.726 2.97l.442 67.251H9.831C1.64 72.292-2.928 62.832 2.166 56.418L45.317 2.071Z"
|
|
fill="#3ECF8E"
|
|
/>
|
|
</svg>
|
|
</LogoBox>
|
|
)
|
|
}
|
|
|
|
export function AccountRow({
|
|
displayName,
|
|
action,
|
|
}: {
|
|
displayName: string
|
|
action?: React.ReactNode
|
|
}) {
|
|
return (
|
|
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50')}>
|
|
<CardContent
|
|
className={cn('flex gap-3 border-none', action ? 'items-center px-4 py-3' : 'p-3')}
|
|
>
|
|
<Avatar className="size-8 border border-muted">
|
|
<AvatarFallback className="text-xs">A</AvatarFallback>
|
|
</Avatar>
|
|
<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}</p>
|
|
</div>
|
|
{action}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export function InterstitialShell({
|
|
logo,
|
|
title,
|
|
description,
|
|
children,
|
|
}: {
|
|
logo: React.ReactNode
|
|
title: string
|
|
description?: string
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<div className="flex min-h-[520px] w-full items-center justify-center bg-studio px-2 py-6">
|
|
<Card className="w-full max-w-[400px] overflow-hidden">
|
|
<CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center">
|
|
<div className="mb-4 flex justify-center">{logo}</div>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<h1 className="text-balance text-lg font-medium tracking-tight text-foreground">
|
|
{title}
|
|
</h1>
|
|
{description ? (
|
|
<p className="m-0 px-3 text-balance text-sm leading-tight text-foreground-lighter">
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</CardHeader>
|
|
<div className="px-6 pb-6">{children}</div>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function SignOutButton() {
|
|
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
|
|
}
|