{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "oauth-consent-react",
  "title": "OAuth Consent for React",
  "description": "OAuth 2.1 consent component for an existing authenticated application.",
  "dependencies": [
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [
    "button",
    "card",
    "https://supabase.com/library/r/safe-next-path.json"
  ],
  "files": [
    {
      "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n  useOAuthConsent,\n  type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n  clientName: string\n  productName: string\n}\n\nfunction ConsentCardShell({\n  clientName,\n  productName,\n  className,\n  children,\n  ...props\n}: ConsentCardShellProps) {\n  return (\n    <div className={cn('flex flex-col gap-6', className)} {...props}>\n      <Card>\n        <CardHeader className=\"items-center space-y-4 text-center\">\n          <div\n            className=\"flex items-center justify-center\"\n            aria-label={`${clientName} connecting to ${productName}`}\n          >\n            <div className=\"flex size-12 items-center justify-center rounded-full border bg-muted font-medium\">\n              {getInitial(clientName)}\n            </div>\n            <div className=\"h-px w-8 bg-border\" aria-hidden=\"true\" />\n            <div className=\"flex size-12 items-center justify-center rounded-full border bg-muted font-medium\">\n              {getInitial(productName)}\n            </div>\n          </div>\n          <div className=\"space-y-1.5\">\n            <CardTitle className=\"text-2xl\">Authorize {clientName}</CardTitle>\n            <CardDescription>Review what this client gets access to.</CardDescription>\n          </div>\n        </CardHeader>\n        <CardContent className=\"space-y-6\">{children}</CardContent>\n      </Card>\n    </div>\n  )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n  clientName: string\n  productName?: string\n  redirectUri: string\n  email: string\n  scopes?: string[]\n  error?: string | null\n  decision?: OAuthConsentDecision | null\n  onApprove?: () => void\n  onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n  clientName,\n  productName = 'Your product',\n  redirectUri,\n  email,\n  scopes = [],\n  error = null,\n  decision = null,\n  onApprove,\n  onDeny,\n  ...props\n}: OAuthConsentCardProps) {\n  return (\n    <ConsentCardShell clientName={clientName} productName={productName} {...props}>\n      <dl className=\"divide-y rounded-lg border text-sm\">\n        <div className=\"flex items-center justify-between gap-6 p-4\">\n          <dt className=\"text-muted-foreground\">Client</dt>\n          <dd className=\"min-w-0 break-all text-right font-medium\">{clientName}</dd>\n        </div>\n        <div className=\"flex items-center justify-between gap-6 p-4\">\n          <dt className=\"text-muted-foreground\">Redirects to</dt>\n          <dd className=\"min-w-0 break-all text-right font-medium\">{redirectUri}</dd>\n        </div>\n        <div className=\"flex items-center justify-between gap-6 p-4\">\n          <dt className=\"text-muted-foreground\">Signed in as</dt>\n          <dd className=\"min-w-0 break-all text-right font-medium\">{email}</dd>\n        </div>\n        {scopes.length > 0 && (\n          <div className=\"flex items-center justify-between gap-6 p-4\">\n            <dt className=\"text-muted-foreground\">Scopes</dt>\n            <dd className=\"min-w-0 break-all text-right font-medium\">{scopes.join(', ')}</dd>\n          </div>\n        )}\n      </dl>\n      <p className=\"text-sm text-muted-foreground\">\n        Allow access only if you recognize this application. It can act on your behalf only within\n        the requested permissions and the access you already have.\n      </p>\n      {error && (\n        <p role=\"alert\" className=\"text-sm text-destructive\">\n          {error}\n        </p>\n      )}\n      <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n        <Button type=\"button\" variant=\"outline\" disabled={decision !== null} onClick={onDeny}>\n          {decision === 'deny' ? 'Denying...' : 'Deny'}\n        </Button>\n        <Button type=\"button\" disabled={decision !== null} onClick={onApprove}>\n          {decision === 'approve' ? 'Allowing...' : 'Allow access'}\n        </Button>\n      </div>\n    </ConsentCardShell>\n  )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n  authorizationId?: string | null\n  signInPath?: string\n  productName?: string\n}\n\nexport function OAuthConsent({\n  authorizationId,\n  signInPath = '/auth/login',\n  productName = 'Your product',\n  ...props\n}: OAuthConsentProps) {\n  const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n    authorizationId,\n    signInPath,\n  })\n\n  if (isLoading || !details || !email) {\n    return (\n      <ConsentCardShell clientName=\"OAuth client\" productName={productName} {...props}>\n        {isLoading ? (\n          <p role=\"status\" className=\"text-sm text-muted-foreground\">\n            Loading authorization request...\n          </p>\n        ) : (\n          <p role=\"alert\" className=\"text-sm text-destructive\">\n            {error ??\n              'Unable to load the authorization request. Start again from your OAuth client.'}\n          </p>\n        )}\n      </ConsentCardShell>\n    )\n  }\n\n  return (\n    <OAuthConsentCard\n      clientName={details.client.name}\n      productName={productName}\n      redirectUri={details.redirect_uri}\n      email={email}\n      scopes={details.scope.split(' ').filter(Boolean)}\n      error={error}\n      decision={decision}\n      onApprove={() => void approve()}\n      onDeny={() => void deny()}\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts",
      "content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n  authorizationId?: string | null\n  signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n  const url = new URL(path, window.location.origin)\n  const searchParams = new URLSearchParams(url.search)\n  searchParams.set('next', next)\n  url.search = searchParams.toString()\n  return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n  authorizationId,\n  signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n  const [details, setDetails] = useState<OAuthAuthorizationDetails | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const [isLoading, setIsLoading] = useState(true)\n  const [decision, setDecision] = useState<OAuthConsentDecision | null>(null)\n  const isDeciding = useRef(false)\n\n  useEffect(() => {\n    let active = true\n\n    const loadAuthorization = async () => {\n      setIsLoading(true)\n      setError(null)\n      setDetails(null)\n      setDecision(null)\n\n      if (!authorizationId) {\n        setError('This page needs an authorization_id. Start again from your OAuth client.')\n        setIsLoading(false)\n        return\n      }\n\n      const supabase = createClient()\n      const {\n        data: { user },\n        error: userError,\n      } = await supabase.auth.getUser()\n\n      if (userError && !isAuthSessionMissingError(userError)) {\n        if (active) {\n          setError(userError.message)\n          setIsLoading(false)\n        }\n        return\n      }\n\n      if (!user) {\n        const next = `${window.location.pathname}${window.location.search}`\n        if (active) {\n          window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n        }\n        return\n      }\n\n      const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n      if (error) {\n        if (active) {\n          setError(error.message)\n          setIsLoading(false)\n        }\n        return\n      }\n\n      if (!('authorization_id' in data)) {\n        if (active) {\n          window.location.replace(data.redirect_url)\n        }\n        return\n      }\n\n      if (active) {\n        setDetails(data)\n        setIsLoading(false)\n      }\n    }\n\n    void loadAuthorization()\n    return () => {\n      active = false\n    }\n  }, [authorizationId, signInPath])\n\n  const decide = useCallback(\n    async (action: OAuthConsentDecision) => {\n      if (!authorizationId || isDeciding.current) return\n\n      isDeciding.current = true\n      setDecision(action)\n      setError(null)\n      const supabase = createClient()\n      const result =\n        action === 'approve'\n          ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n              skipBrowserRedirect: true,\n            })\n          : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n              skipBrowserRedirect: true,\n            })\n\n      if (result.error) {\n        setError(result.error.message)\n        setDecision(null)\n        isDeciding.current = false\n        return\n      }\n\n      if (!result.data?.redirect_url) {\n        setError('The server did not return a redirect. Start again from your OAuth client.')\n        setDecision(null)\n        isDeciding.current = false\n        return\n      }\n\n      window.location.assign(result.data.redirect_url)\n    },\n    [authorizationId]\n  )\n\n  return {\n    details,\n    email: details?.user.email ?? null,\n    error,\n    isLoading,\n    decision,\n    approve: () => decide('approve'),\n    deny: () => decide('deny'),\n  }\n}\n\ntype UseOAuthConsentReturn = ReturnType<typeof useOAuthConsent>\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/clients/react/lib/supabase/client.ts",
      "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n  return createSupabaseClient(\n    import.meta.env.VITE_SUPABASE_URL!,\n    import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "VITE_SUPABASE_URL": "",
    "VITE_SUPABASE_PUBLISHABLE_KEY": ""
  },
  "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` — edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
  "type": "registry:block"
}