Files
supabase/apps/studio/components/interfaces/Account/AccessTokens/hooks/useMergedAccessTokens.ts
Gildas Garcia cddb430310 feat(studio): scoped pat root branch (#48384)
## Description

This is the Scoped PAT stacked PRs root branch

## How to test

### With the `scopedPAT` enabled (default on staging)

Go to
https://studio-staging-git-scopedpat-merge-token-lists-supabase.vercel.app/dashboard/account/tokens.
- You shouldn't see two tabs anymore
- If you had classic tokens, they should have the _Legacy_ badge
- You can create scoped tokens
- You have a way to copy newly created tokens before closing the form
side panel

### With the `scopedPAT` disabled (use the devtool to override)
- You shouldn't see two tabs anymore
- If you had classic tokens, they should **not** have the _Legacy_ badge
- You can create classic tokens
- You have a way to copy newly created tokens above the list upon form
submission

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

* **New Features**
* Show classic and scoped access tokens together in one list, with
classic tokens labeled “Legacy” when the scoped experience is enabled.
* Add scoped access token creation with a two-step configure → review →
success flow (when enabled).
* Add a dismissible migration notice about scoped tokens with a link to
API docs.
  * Show “View permissions” only for scoped tokens.
* **Bug Fixes**
* Token deletion now supports both classic and scoped tokens with the
correct confirmation and success handling.
* The scoped tokens page now redirects to the unified access tokens
page.
* **Accessibility**
* Improved accessibility by adding a label to the token “more options”
action.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
Co-authored-by: kemal.earth <606977+kemaldotearth@users.noreply.github.com>
2026-08-06 07:40:56 -06:00

66 lines
2.1 KiB
TypeScript

import { useMemo } from 'react'
import type { BaseToken } from '../AccessToken.types'
import { useAccessTokensQuery } from '@/data/access-tokens/access-tokens-query'
import { useScopedAccessTokensQuery } from '@/data/scoped-access-tokens/scoped-access-token-query'
export type TokenKind = 'classic' | 'scoped'
export type MergedAccessToken = ClassicAccessToken | ScopedAccessToken
export interface ClassicAccessToken extends BaseToken {
id: number
kind: 'classic'
}
export interface ScopedAccessToken extends BaseToken {
id: string
kind: 'scoped'
}
interface UseMergedAccessTokensOptions {
/** Whether scoped tokens should be fetched + merged (gated on the scopedPAT flag). */
scopedTokensEnabled?: boolean
}
/**
* Merges classic and scoped access tokens into a single list, tagging each row with its `kind`.
* Classic is the baseline (its loading/error state drives the list); scoped failures degrade
* gracefully so a missing/undeployed scoped endpoint never hides classic tokens. Sorting is left to
* the consuming list (via filterAndSortTokens), so this only merges + tags.
*/
export const useMergedAccessTokens = ({
scopedTokensEnabled,
}: UseMergedAccessTokensOptions = {}) => {
const classic = useAccessTokensQuery()
const scoped = useScopedAccessTokensQuery({ enabled: scopedTokensEnabled })
return useMemo(() => {
const classicTokens: MergedAccessToken[] = (classic.data ?? []).map((token) => ({
...token,
kind: 'classic',
}))
const scopedTokens: MergedAccessToken[] = (scoped.data?.tokens ?? []).map((token) => ({
...token,
kind: 'scoped',
}))
return {
tokens: [...classicTokens, ...scopedTokens],
// Classic drives the primary states; a scoped fetch that is still loading shouldn't block the list.
isLoading: classic.isPending || (scopedTokensEnabled && scoped.isPending),
isError: classic.isError,
error: classic.error,
isSuccess: classic.isSuccess,
}
}, [
classic.data,
classic.isPending,
classic.isError,
classic.error,
classic.isSuccess,
scoped.data,
scoped.isPending,
scopedTokensEnabled,
])
}