Files
supabase/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx
Danny WhiteandJoshen Lim e19cd1863d feat(studio): connect logo contract for authorize (#48161)
## 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>
2026-07-23 02:20:42 +10:00

228 lines
7.3 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.