mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +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>
228 lines
7.3 KiB
Plaintext
228 lines
7.3 KiB
Plaintext
---
|
||
title: Connect Interstitials
|
||
description: Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows.
|
||
---
|
||
|
||
Connect interstitials are focused, single-card flows that sit outside the main
|
||
Studio shell. Use the shared `InterstitialLayout` family instead of building
|
||
bespoke centered cards, logos, account rows, or organisation selectors.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-demo"
|
||
description="Centered 400px card with partner branding, account row, and a single primary action"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
## Use this pattern for
|
||
|
||
This pattern fits short-lived connect flows: partner authorisation and consent
|
||
(OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing
|
||
connections (AWS Marketplace, Vercel install, credit redemption), and CLI or
|
||
device-code sign-in. Use the same shell for their loading, error, success, and
|
||
wrong-account states.
|
||
|
||
Do not use it for normal authenticated Studio pages. Those should use the
|
||
standard [page layout](./layout) patterns.
|
||
|
||
## Source of truth
|
||
|
||
```tsx
|
||
import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector'
|
||
import {
|
||
InterstitialAccountRow,
|
||
InterstitialLayout,
|
||
LogoBox,
|
||
LogoPair,
|
||
PartnerLogo,
|
||
SupabaseLogo,
|
||
} from '@/components/layouts/InterstitialLayout'
|
||
```
|
||
|
||
## Basic shape
|
||
|
||
Use `InterstitialLayout` for the outer card, then put route-specific content in
|
||
`px-6 pb-6`. Widen the card only when the flow embeds a real tool, such as
|
||
project linking.
|
||
|
||
```tsx
|
||
<InterstitialLayout
|
||
logo={
|
||
<LogoPair
|
||
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
|
||
right={<SupabaseLogo />}
|
||
/>
|
||
}
|
||
title="Authorize Stripe Projects"
|
||
description="This will create an organization on your behalf in Supabase"
|
||
>
|
||
<div className="px-6 pb-6">
|
||
<InterstitialAccountRow displayName={displayName} />
|
||
<Button variant="primary" block>
|
||
Continue
|
||
</Button>
|
||
</div>
|
||
</InterstitialLayout>
|
||
```
|
||
|
||
```tsx
|
||
<InterstitialLayout
|
||
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
|
||
title="Connect Vercel project"
|
||
containerClassName="items-start"
|
||
cardClassName="max-w-[900px]"
|
||
>
|
||
<div className="px-6 pb-6">{projectLinker}</div>
|
||
</InterstitialLayout>
|
||
```
|
||
|
||
## Logos
|
||
|
||
Use `LogoPair` when the user is connecting two known services, and
|
||
`SupabaseLogo` alone for first-party flows or when the requester has no trusted
|
||
mark. `PartnerLogo` fills the 48px box edge-to-edge; `LogoBox` is for custom
|
||
inset marks or logos that need their own background. Store new partner icons in
|
||
`apps/studio/public/img/icons`.
|
||
|
||
### Pairing
|
||
|
||
| Requester logo | Header treatment |
|
||
| -------------------------------------------------------- | ---------------------------------------------------- |
|
||
| Curated partner / MCP client, or a trusted uploaded icon | `LogoPair` with requester left, `SupabaseLogo` right |
|
||
| Unknown, missing, blocked, or failed-to-load icon | `SupabaseLogo` alone. Do not invent an initial tile. |
|
||
|
||
The user is usually arriving from the third-party app. The interstitial should
|
||
confirm they are connecting to Supabase. Put the requester name in the title and
|
||
description; do not manufacture a letter avatar to fill the left side of a pair.
|
||
|
||
**Known services.** When both sides are curated (or otherwise known), pair them.
|
||
Theme-reactive tiles are fine when both marks have matching light/dark
|
||
treatment.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-pair"
|
||
description="LogoPair when the user is connecting two services"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
**No trusted requester mark.** If the icon is missing, blocked, or fails to
|
||
load, show `SupabaseLogo` alone. Do not invent an initial tile to fill the
|
||
pair.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-unknown"
|
||
description="No trusted requester mark: Supabase alone"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
**Uploaded organisation OAuth icons.** Icons published via Studio’s OAuth app
|
||
builder are unclassified bitmaps — we do not know if they were authored for
|
||
light or dark. Treat the pair as light on both Studio themes: fixed light tile
|
||
chrome (`border-black/10 bg-white`, `SupabaseLogo forceLight`) on both sides.
|
||
Do not invent a dark variant for the upload. Toggle the docs theme to dark to
|
||
see the light tiles hold against the Studio chrome.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-uploaded"
|
||
description="Uploaded OAuth app icon: forced-light tiles on both sides"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
### Assets
|
||
|
||
Treat Connect logos as assets, not theme tokens.
|
||
|
||
**Default to light.** Prefer a single static light mark inside `LogoBox`.
|
||
Light assets read fine on both light and dark Studio themes. That is the
|
||
default for Connect tiles.
|
||
|
||
**Keep pairs matched.** In a `LogoPair`, both marks must use the same
|
||
treatment: both light, or both dark. Do not mix a light partner tile with a
|
||
dark-theme-only Supabase treatment, or the reverse. Theme-aware dark variants
|
||
are fine for curated partners that already have them, but then both sides of
|
||
the pair should use the dark set together.
|
||
|
||
**What not to do**
|
||
|
||
- Do not invent light/dark pairs for arbitrary remote OAuth icons.
|
||
- Do not recolour vendor SVGs with theme CSS. Monochrome identity-provider
|
||
masks (for example GitHub on sign-in) stay a separate pattern.
|
||
|
||
**Where logos come from on `/authorize`**
|
||
|
||
- Curated partner logos resolve from allowlisted `redirect_uri` hosts only,
|
||
not from self-asserted `name` or `website`. Those pairs may use theme tiles
|
||
and dark assets when the partner has them.
|
||
- Published organisation OAuth app icons uploaded in Studio remain trusted
|
||
remote images, paired with forced-light tiles on both sides.
|
||
- Everything else falls back to `SupabaseLogo` alone.
|
||
|
||
## Account row
|
||
|
||
Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally.
|
||
|
||
```tsx
|
||
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />
|
||
```
|
||
|
||
## Organisation selection
|
||
|
||
Use `OrganizationSelector` when the flow needs an organisation pick. Extend it
|
||
for new states instead of inventing a parallel card style.
|
||
|
||
```tsx
|
||
<OrganizationSelector
|
||
organizations={linkableOrganizations}
|
||
selectedSlug={selectedOrgSlug}
|
||
onSelect={setSelectedOrgSlug}
|
||
createLabel="Create new organization"
|
||
onCreate={() => setShowOrgCreationDialog(true)}
|
||
/>
|
||
```
|
||
|
||
## Actions
|
||
|
||
Prefer one full-width primary action. A full-width text button is fine for a
|
||
secondary action that still belongs in the flow.
|
||
|
||
## States
|
||
|
||
Keep loading, invalid, error, and success states inside the same card when the
|
||
route can explain them. Use `ShimmeringLoader` for loading, and `Admonition`
|
||
for warning, error, note, and success copy.
|
||
|
||
<ComponentPreview
|
||
name="connect-interstitial-logo-single"
|
||
description="Wrong-account warning inside the same interstitial card"
|
||
align="start"
|
||
className="p-0"
|
||
padded={false}
|
||
peekCode
|
||
wide
|
||
/>
|
||
|
||
## Copy
|
||
|
||
Use sentence case. Prefer `sign in` over `login`. Titles and primary actions
|
||
should follow `Verb -> Thing`, for example `Authorize Stripe Projects` or
|
||
`Install Vercel`.
|
||
|
||
Keep the layout title static across states and put state-specific copy in the
|
||
body. Header descriptions should stay short and should not end with a full
|
||
stop.
|