{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "oauth-provider",
  "title": "OAuth Provider",
  "description": "OAuth Provider plugin: authorization consent screen, prompt=create sign-up continuation, account chooser, and a connected applications security card for Better Auth authorization requests.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "@better-auth/oauth-provider",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "avatar",
    "badge",
    "button",
    "card",
    "empty",
    "item",
    "separator",
    "skeleton",
    "spinner",
    "https://better-auth-ui.com/r/radix-nova/sign-up.json",
    "https://better-auth-ui.com/r/radix-nova/user-avatar.json"
  ],
  "files": [
    {
      "path": "src/lib/auth/auth-plugin.ts",
      "content": "import type {\n  AuthPluginComponents,\n  AuthPlugin as AuthPluginPrimitive\n} from \"@better-auth-ui/react\"\n\ndeclare module \"@better-auth-ui/core\" {\n  /** Widens `useAuth().plugins` to the shadcn-typed `AuthPlugin`. */\n  interface AuthPluginRegister {\n    shadcn: AuthPlugin\n  }\n}\n\n/** Props the shadcn `<Auth>` router spreads onto plugin-contributed auth views. */\nexport type AuthViewProps = {\n  className?: string\n  socialLayout?: \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Props the shadcn `<Settings>` router spreads onto plugin-contributed settings views. */\nexport type SettingsViewProps = {\n  className?: string\n}\n\n/** Shadcn plugin type. Plugin authors import this from `@/lib/auth/auth-plugin`. */\nexport type AuthPlugin = AuthPluginPrimitive<\n  AuthPluginComponents,\n  AuthViewProps,\n  SettingsViewProps\n>\n",
      "type": "registry:lib",
      "target": "@lib/auth/auth-plugin.ts"
    },
    {
      "path": "src/lib/auth/oauth-provider-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  oauthProviderPlugin as coreOAuthProviderPlugin,\n  type OAuthProviderPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { AuthorizedApplications } from \"@/components/auth/oauth-provider/authorized-applications\"\nimport { OAuthConsent } from \"@/components/auth/oauth-provider/oauth-consent\"\nimport { OAuthSelectAccount } from \"@/components/auth/oauth-provider/oauth-select-account\"\nimport { OAuthSignUp } from \"@/components/auth/oauth-provider/oauth-sign-up\"\n\nexport const oauthProviderPlugin = createAuthPlugin(\n  coreOAuthProviderPlugin.id,\n  (options: OAuthProviderPluginOptions = {}) => {\n    const core = coreOAuthProviderPlugin(options)\n\n    return {\n      ...core,\n      views: {\n        auth: {\n          oauthConsent: OAuthConsent,\n          // A route of its own rather than an override of the built-in\n          // `signUp` view — ordinary sign-up stays untouched.\n          oauthSignUp: OAuthSignUp,\n          oauthSelectAccount: OAuthSelectAccount\n        }\n      },\n      ...(core.showConnectedApplications\n        ? { securityCards: [AuthorizedApplications] }\n        : {})\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/oauth-provider-plugin.ts"
    },
    {
      "path": "src/components/auth/oauth-provider/oauth-consent.tsx",
      "content": "\"use client\"\n\nimport {\n  type OAuthAuthorizationRequest,\n  parseOAuthAuthorizationRequest,\n  resolveOAuthScopeMetadata,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins\"\nimport {\n  type OAuthProviderAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useOAuthConsent,\n  usePublicOAuthClient,\n  useSession\n} from \"@better-auth-ui/react\"\nimport { Check, ShieldCheck } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { UserAvatar } from \"../user/user-avatar\"\n\nexport type OAuthConsentProps = {\n  className?: string\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\nexport function OAuthConsent({ className }: OAuthConsentProps) {\n  const { authClient } = useAuth()\n  const { localization, scopeMetadata } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderAuthClient\n  const { data: session, isPending: isSessionPending } = useSession(oauthClient)\n  const [request, setRequest] = useState<OAuthAuthorizationRequest>()\n\n  useEffect(() => {\n    setRequest(parseOAuthAuthorizationRequest(window.location.search))\n  }, [])\n\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: Boolean(session && request?.clientId)\n  })\n  const consent = useOAuthConsent(oauthClient)\n  const client = publicClient.data\n  const clientName = client?.client_name || localization.application\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n  const policyUrl = sanitizeOAuthClientUrl(client?.policy_uri)\n  const termsUrl = sanitizeOAuthClientUrl(client?.tos_uri)\n  const requestResolved = request !== undefined\n  const invalidRequest =\n    requestResolved &&\n    (!request.clientId ||\n      (!isSessionPending && !session) ||\n      publicClient.isError ||\n      (!publicClient.isPending && session && !client))\n  const canRespond = Boolean(\n    request?.clientId && session && client && !consent.isPending\n  )\n\n  if (invalidRequest) {\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardHeader>\n          <CardTitle className=\"text-xl\">\n            {localization.invalidRequest}\n          </CardTitle>\n          <CardDescription>\n            {localization.invalidRequestDescription}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    )\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-md\", className)}>\n      <CardHeader className=\"gap-4\">\n        <div className=\"flex items-center gap-3\">\n          {client ? (\n            <Avatar size=\"lg\">\n              <AvatarImage\n                alt={clientName}\n                referrerPolicy=\"no-referrer\"\n                src={logoUrl}\n              />\n              <AvatarFallback>\n                <ShieldCheck className=\"size-5\" />\n              </AvatarFallback>\n            </Avatar>\n          ) : (\n            <Skeleton className=\"size-10 rounded-full\" />\n          )}\n\n          <div className=\"min-w-0 flex-1\">\n            {client ? (\n              <p className=\"truncate font-medium\">{clientName}</p>\n            ) : (\n              <Skeleton className=\"h-4 w-36\" />\n            )}\n            {client?.client_uri ? (\n              <p className=\"truncate text-xs text-muted-foreground\">\n                {client.client_uri}\n              </p>\n            ) : null}\n          </div>\n        </div>\n\n        <div className=\"grid gap-1\">\n          <CardTitle className=\"text-xl\">\n            {interpolateClient(localization.authorize, clientName)}\n          </CardTitle>\n          <CardDescription>\n            {interpolateClient(\n              localization.authorizationDescription,\n              clientName\n            )}\n          </CardDescription>\n        </div>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-5\">\n        <div className=\"grid gap-3\">\n          <p className=\"text-sm font-medium\">\n            {interpolateClient(localization.requestedPermissions, clientName)}\n          </p>\n\n          {request ? (\n            <ul className=\"grid gap-3\">\n              {request.scopes.map((scope) => {\n                const metadata = resolveOAuthScopeMetadata(\n                  scopeMetadata,\n                  scope,\n                  {\n                    clientId: request.clientId,\n                    requestedScopes: request.scopes\n                  }\n                )\n\n                return (\n                  <li className=\"flex gap-3\" key={scope}>\n                    <Check className=\"mt-0.5 size-4 shrink-0 text-primary\" />\n                    <div className=\"grid gap-0.5\">\n                      <p className=\"text-sm font-medium\">{metadata.label}</p>\n                      {metadata.description ? (\n                        <p className=\"text-xs text-muted-foreground\">\n                          {metadata.description}\n                        </p>\n                      ) : null}\n                    </div>\n                  </li>\n                )\n              })}\n            </ul>\n          ) : (\n            <div className=\"flex gap-3\">\n              <Skeleton className=\"mt-0.5 size-4 shrink-0 rounded-full\" />\n              <div className=\"grid flex-1 gap-2\">\n                <Skeleton className=\"h-4 w-32\" />\n                <Skeleton className=\"h-3 w-full max-w-64\" />\n              </div>\n            </div>\n          )}\n        </div>\n\n        <Separator />\n\n        <div className=\"flex items-center gap-3\">\n          <UserAvatar isPending={isSessionPending} user={session?.user} />\n          <div className=\"min-w-0 flex-1\">\n            <p className=\"text-xs text-muted-foreground\">\n              {localization.signedInAs}\n            </p>\n            {session ? (\n              <>\n                <p className=\"truncate text-sm font-medium\">\n                  {session.user.name || session.user.email}\n                </p>\n                {session.user.name ? (\n                  <p className=\"truncate text-xs text-muted-foreground\">\n                    {session.user.email}\n                  </p>\n                ) : null}\n              </>\n            ) : (\n              <Skeleton className=\"mt-1 h-4 w-40\" />\n            )}\n          </div>\n        </div>\n\n        {policyUrl || termsUrl ? (\n          <div className=\"flex flex-wrap gap-x-4 gap-y-2 text-xs\">\n            {policyUrl ? (\n              <a\n                className=\"text-muted-foreground underline underline-offset-4 hover:text-foreground\"\n                href={policyUrl}\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                {localization.privacyPolicy}\n              </a>\n            ) : null}\n            {termsUrl ? (\n              <a\n                className=\"text-muted-foreground underline underline-offset-4 hover:text-foreground\"\n                href={termsUrl}\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                {localization.termsOfService}\n              </a>\n            ) : null}\n          </div>\n        ) : null}\n      </CardContent>\n\n      <CardFooter className=\"grid grid-cols-2 gap-2\">\n        <Button\n          disabled={!canRespond}\n          variant=\"outline\"\n          onClick={() => consent.mutate({ accept: false })}\n        >\n          {consent.isPending && consent.variables?.accept === false ? (\n            <Spinner />\n          ) : null}\n          {localization.cancel}\n        </Button>\n        <Button\n          disabled={!canRespond}\n          onClick={() => consent.mutate({ accept: true })}\n        >\n          {consent.isPending && consent.variables?.accept === true ? (\n            <Spinner />\n          ) : null}\n          {localization.allow}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-consent.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/oauth-sign-up.tsx",
      "content": "\"use client\"\n\nimport {\n  hasOAuthPrompt,\n  type OAuthAuthorizationRequest,\n  parseOAuthAuthorizationRequest\n} from \"@better-auth-ui/core/plugins\"\nimport {\n  type OAuthProviderAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useOAuthContinue,\n  usePublicOAuthClient\n} from \"@better-auth-ui/react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport type { SocialLayout } from \"../provider-buttons\"\nimport { SignUp } from \"../sign-up\"\n\nexport type OAuthSignUpProps = {\n  className?: string\n  socialLayout?: SocialLayout\n  socialPosition?: \"top\" | \"bottom\"\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\n/**\n * Sign-up view that resumes a signed OAuth authorization request.\n *\n * When Better Auth sends the user here with `prompt=create`, the ordinary\n * sign-up form still does the account creation. Only once that succeeds and\n * leaves a usable session does this call `oauth2.continue({ created: true })`\n * so Better Auth can finish the authorization it started.\n *\n * Without `prompt=create` this is just the normal sign-up view.\n */\nexport function OAuthSignUp({\n  className,\n  socialLayout,\n  socialPosition\n}: OAuthSignUpProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderAuthClient\n\n  const [request, setRequest] = useState<OAuthAuthorizationRequest>()\n  const [isCreated, setIsCreated] = useState(false)\n\n  useEffect(() => {\n    setRequest(parseOAuthAuthorizationRequest(window.location.search))\n  }, [])\n\n  const isOAuthSignUp = Boolean(request && hasOAuthPrompt(request, \"create\"))\n\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: isOAuthSignUp\n  })\n  const clientName = publicClient.data?.client_name || localization.application\n\n  const oauthContinue = useOAuthContinue(oauthClient)\n\n  // The account already exists at this point, so retrying continuation is the\n  // only sensible recovery — never send the user back through the form.\n  if (isCreated) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardHeader>\n          <CardTitle className=\"text-xl font-semibold\">\n            {localization.accountCreated}\n          </CardTitle>\n\n          <CardDescription>\n            {interpolateClient(\n              oauthContinue.isError\n                ? localization.continueFailed\n                : localization.continuing,\n              clientName\n            )}\n          </CardDescription>\n        </CardHeader>\n\n        {oauthContinue.isError && (\n          <CardFooter>\n            <Button\n              className=\"w-full\"\n              disabled={oauthContinue.isPending}\n              onClick={() => oauthContinue.mutate({ created: true })}\n            >\n              {oauthContinue.isPending && <Spinner />}\n\n              {localization.tryAgain}\n            </Button>\n          </CardFooter>\n        )}\n      </Card>\n    )\n  }\n\n  return (\n    <SignUp\n      className={className}\n      socialLayout={socialLayout}\n      socialPosition={socialPosition}\n      onSignUpSuccess={\n        isOAuthSignUp\n          ? () => {\n              setIsCreated(true)\n              oauthContinue.mutate({ created: true })\n            }\n          : undefined\n      }\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-sign-up.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/oauth-select-account.tsx",
      "content": "\"use client\"\n\nimport {\n  type OAuthAuthorizationRequest,\n  parseOAuthAuthorizationRequest,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins\"\nimport {\n  type ListDeviceSession,\n  type OAuthProviderMultiSessionAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useListDeviceSessions,\n  useOAuthContinue,\n  usePublicOAuthClient,\n  useSession,\n  useSetActiveSession\n} from \"@better-auth-ui/react\"\nimport { ShieldCheck } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemGroup,\n  ItemMedia,\n  ItemTitle\n} from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { UserAvatar } from \"../user/user-avatar\"\n\nexport type OAuthSelectAccountProps = {\n  className?: string\n}\n\nconst interpolateClient = (template: string, clientName: string) =>\n  template.replace(\"{{client}}\", clientName)\n\n/**\n * Account chooser for a signed OAuth authorization request.\n *\n * Switching accounts has to land before Better Auth resumes the request, so\n * picking a different session calls `multiSession.setActive()` first and only\n * then `oauth2.continue({ selected: true })`. Picking the account that is\n * already active skips the switch entirely.\n *\n * This screen deliberately has no sign-out or revoke actions — session\n * management belongs in security settings.\n */\nexport function OAuthSelectAccount({ className }: OAuthSelectAccountProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n  const oauthClient = authClient as OAuthProviderMultiSessionAuthClient\n\n  const { data: session, isPending: isSessionPending } = useSession(oauthClient)\n  const [request, setRequest] = useState<OAuthAuthorizationRequest>()\n  const [pendingSessionId, setPendingSessionId] = useState<string>()\n\n  useEffect(() => {\n    setRequest(parseOAuthAuthorizationRequest(window.location.search))\n  }, [])\n\n  const publicClient = usePublicOAuthClient(oauthClient, request?.clientId, {\n    enabled: Boolean(session && request?.clientId)\n  })\n  const { data: deviceSessions, isPending: isDeviceSessionsPending } =\n    useListDeviceSessions(oauthClient)\n\n  const client = publicClient.data\n  const clientName = client?.client_name || localization.application\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n\n  const setActiveSession = useSetActiveSession(oauthClient)\n  const oauthContinue = useOAuthContinue(oauthClient)\n\n  const requestResolved = request !== undefined\n  const invalidRequest =\n    requestResolved &&\n    (!request.clientId ||\n      (!isSessionPending && !session) ||\n      publicClient.isError ||\n      (!publicClient.isPending && session && !client))\n\n  const selectAccount = async (\n    deviceSession: ListDeviceSession<OAuthProviderMultiSessionAuthClient>\n  ) => {\n    setPendingSessionId(deviceSession.session.id)\n\n    try {\n      if (deviceSession.session.id !== session?.session.id) {\n        await setActiveSession.mutateAsync({\n          sessionToken: deviceSession.session.token\n        })\n      }\n\n      await oauthContinue.mutateAsync({ selected: true })\n    } catch {\n      // The error toaster surfaces the failure; re-enable the rows so the\n      // user can pick again.\n      setPendingSessionId(undefined)\n    }\n  }\n\n  if (invalidRequest) {\n    return (\n      <Card className={cn(\"w-full max-w-md\", className)}>\n        <CardHeader>\n          <CardTitle className=\"text-xl\">\n            {localization.invalidRequest}\n          </CardTitle>\n          <CardDescription>\n            {localization.invalidRequestDescription}\n          </CardDescription>\n        </CardHeader>\n      </Card>\n    )\n  }\n\n  const isBusy = pendingSessionId !== undefined\n\n  return (\n    <Card className={cn(\"w-full max-w-md\", className)}>\n      <CardHeader className=\"gap-4\">\n        <div className=\"flex items-center gap-3\">\n          {client ? (\n            <Avatar size=\"lg\">\n              <AvatarImage\n                alt={clientName}\n                referrerPolicy=\"no-referrer\"\n                src={logoUrl}\n              />\n              <AvatarFallback>\n                <ShieldCheck className=\"size-5\" />\n              </AvatarFallback>\n            </Avatar>\n          ) : (\n            <Skeleton className=\"size-10 rounded-full\" />\n          )}\n\n          <div className=\"min-w-0 flex-1\">\n            {client ? (\n              <p className=\"truncate font-medium\">{clientName}</p>\n            ) : (\n              <Skeleton className=\"h-4 w-36\" />\n            )}\n            {client?.client_uri ? (\n              <p className=\"truncate text-xs text-muted-foreground\">\n                {client.client_uri}\n              </p>\n            ) : null}\n          </div>\n        </div>\n\n        <div className=\"grid gap-1\">\n          <CardTitle className=\"text-xl\">\n            {localization.selectAccount}\n          </CardTitle>\n          <CardDescription>\n            {interpolateClient(\n              localization.selectAccountDescription,\n              clientName\n            )}\n          </CardDescription>\n        </div>\n      </CardHeader>\n\n      <CardContent>\n        {isDeviceSessionsPending ? (\n          <ItemGroup>\n            <Item variant=\"outline\">\n              <ItemMedia>\n                <UserAvatar isPending />\n              </ItemMedia>\n              <ItemContent>\n                <Skeleton className=\"h-4 w-28\" />\n                <Skeleton className=\"h-3 w-40\" />\n              </ItemContent>\n            </Item>\n          </ItemGroup>\n        ) : !deviceSessions?.length ? (\n          <div className=\"flex flex-col items-center gap-1 py-6 text-center\">\n            <p className=\"text-sm font-semibold\">{localization.noAccounts}</p>\n            <p className=\"text-xs text-muted-foreground\">\n              {interpolateClient(\n                localization.noAccountsDescription,\n                clientName\n              )}\n            </p>\n          </div>\n        ) : (\n          <ItemGroup className=\"gap-2\">\n            {deviceSessions.map((deviceSession) => {\n              const isCurrent = deviceSession.session.id === session?.session.id\n              const isSelecting = pendingSessionId === deviceSession.session.id\n\n              return (\n                <Item key={deviceSession.session.id} variant=\"outline\">\n                  <ItemMedia>\n                    <UserAvatar user={deviceSession.user} />\n                  </ItemMedia>\n\n                  <ItemContent>\n                    <ItemTitle className=\"truncate\">\n                      {deviceSession.user.name || deviceSession.user.email}\n                    </ItemTitle>\n                    {deviceSession.user.name ? (\n                      <ItemDescription className=\"truncate\">\n                        {deviceSession.user.email}\n                      </ItemDescription>\n                    ) : null}\n                  </ItemContent>\n\n                  <ItemActions>\n                    {isCurrent && (\n                      <Badge variant=\"secondary\">\n                        {localization.currentAccount}\n                      </Badge>\n                    )}\n\n                    <Button\n                      size=\"sm\"\n                      disabled={isBusy}\n                      onClick={() => selectAccount(deviceSession)}\n                    >\n                      {isSelecting && <Spinner />}\n\n                      {localization.continue}\n                    </Button>\n                  </ItemActions>\n                </Item>\n              )\n            })}\n          </ItemGroup>\n        )}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/oauth-select-account.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/authorized-applications.tsx",
      "content": "\"use client\"\n\nimport { groupOAuthConsents } from \"@better-auth-ui/core/plugins\"\nimport {\n  type OAuthProviderAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useListOAuthConsents\n} from \"@better-auth-ui/react\"\nimport { Fragment } from \"react\"\n\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { ItemGroup, ItemSeparator } from \"@/components/ui/item\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { cn } from \"@/lib/utils\"\nimport { AuthorizedApplication } from \"./authorized-application\"\nimport { AuthorizedApplicationSkeleton } from \"./authorized-application-skeleton\"\nimport { AuthorizedApplicationsEmpty } from \"./authorized-applications-empty\"\n\nexport type AuthorizedApplicationsProps = {\n  className?: string\n}\n\n/**\n * Security card listing the OAuth applications this account has authorized.\n *\n * It manages consent records, not sign-in sessions and not live access\n * tokens. Better Auth can store more than one consent per client, so records\n * are grouped by client ID and rendered as a single application.\n */\nexport function AuthorizedApplications({\n  className\n}: AuthorizedApplicationsProps) {\n  const { authClient } = useAuth()\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n\n  const { data: consents, isPending } = useListOAuthConsents(\n    authClient as OAuthProviderAuthClient\n  )\n\n  const applications = groupOAuthConsents(consents)\n\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)}>\n      <h2 className=\"truncate text-sm font-semibold\">\n        {localization.connectedApplications}\n      </h2>\n\n      <Card className=\"p-0\">\n        <CardContent className=\"p-0\">\n          {isPending ? (\n            <AuthorizedApplicationSkeleton />\n          ) : !applications.length ? (\n            <AuthorizedApplicationsEmpty />\n          ) : (\n            <ItemGroup className=\"gap-0\">\n              {applications.map((application, index) => (\n                <Fragment key={application.clientId}>\n                  {index > 0 && <ItemSeparator />}\n                  <AuthorizedApplication application={application} />\n                </Fragment>\n              ))}\n            </ItemGroup>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-applications.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/authorized-application.tsx",
      "content": "\"use client\"\n\nimport {\n  type AuthorizedOAuthApplication,\n  resolveOAuthScopeMetadata,\n  sanitizeOAuthClientUrl\n} from \"@better-auth-ui/core/plugins\"\nimport {\n  type OAuthProviderAuthClient,\n  useAuth,\n  useAuthPlugin,\n  usePublicOAuthClient\n} from \"@better-auth-ui/react\"\nimport { ShieldCheck } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemMedia,\n  ItemTitle\n} from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\nimport { RemoveAuthorizationDialog } from \"./remove-authorization-dialog\"\n\nexport type AuthorizedApplicationProps = {\n  /** @remarks `AuthorizedOAuthApplication` */\n  application: AuthorizedOAuthApplication\n}\n\n/**\n * A single authorized application row.\n *\n * Each row loads its own public client metadata so one slow or missing\n * application never blocks the rest of the card.\n */\nexport function AuthorizedApplication({\n  application\n}: AuthorizedApplicationProps) {\n  const { authClient } = useAuth()\n  const { localization, scopeMetadata } = useAuthPlugin(oauthProviderPlugin)\n  const [removeOpen, setRemoveOpen] = useState(false)\n\n  const publicClient = usePublicOAuthClient(\n    authClient as OAuthProviderAuthClient,\n    application.clientId\n  )\n\n  const client = publicClient.data\n  const clientName = client?.client_name || application.clientId\n  const logoUrl = sanitizeOAuthClientUrl(client?.logo_uri)\n  const websiteUrl = sanitizeOAuthClientUrl(client?.client_uri)\n\n  return (\n    <Item>\n      <ItemMedia variant=\"image\">\n        {publicClient.isPending ? (\n          <Skeleton className=\"size-10 shrink-0 rounded-md\" />\n        ) : (\n          <Avatar className=\"size-10 shrink-0 rounded-md\">\n            <AvatarImage\n              alt={clientName}\n              referrerPolicy=\"no-referrer\"\n              src={logoUrl}\n            />\n            <AvatarFallback className=\"rounded-md\">\n              <ShieldCheck className=\"size-4.5\" />\n            </AvatarFallback>\n          </Avatar>\n        )}\n      </ItemMedia>\n      <ItemContent>\n        {publicClient.isPending ? (\n          <Skeleton className=\"h-4 w-32\" />\n        ) : (\n          <ItemTitle>{clientName}</ItemTitle>\n        )}\n\n        {websiteUrl ? (\n          <ItemDescription>\n            <a\n              className=\"truncate text-xs text-muted-foreground underline-offset-4 hover:underline\"\n              href={websiteUrl}\n              rel=\"noreferrer\"\n              target=\"_blank\"\n            >\n              {websiteUrl}\n            </a>\n          </ItemDescription>\n        ) : null}\n\n        {application.updatedAt ? (\n          <ItemDescription>\n            {`${localization.lastAuthorized} ${application.updatedAt.toLocaleDateString(\n              undefined,\n              { dateStyle: \"medium\" }\n            )}`}\n          </ItemDescription>\n        ) : null}\n\n        {application.scopes.length > 0 && (\n          <div className=\"flex flex-wrap gap-1.5\">\n            {application.scopes.map((scope) => (\n              <Badge key={scope} variant=\"secondary\">\n                {\n                  resolveOAuthScopeMetadata(scopeMetadata, scope, {\n                    clientId: application.clientId,\n                    requestedScopes: application.scopes\n                  }).label\n                }\n              </Badge>\n            ))}\n          </div>\n        )}\n      </ItemContent>\n      <ItemActions>\n        <Button size=\"sm\" variant=\"outline\" onClick={() => setRemoveOpen(true)}>\n          {localization.removeAuthorization}\n        </Button>\n\n        <RemoveAuthorizationDialog\n          application={application}\n          clientName={clientName}\n          open={removeOpen}\n          onOpenChange={setRemoveOpen}\n        />\n      </ItemActions>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-application.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/authorized-applications-empty.tsx",
      "content": "\"use client\"\n\nimport { useAuthPlugin } from \"@better-auth-ui/react\"\nimport { ShieldCheck } from \"lucide-react\"\n\nimport {\n  Empty,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle\n} from \"@/components/ui/empty\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\n\nexport function AuthorizedApplicationsEmpty() {\n  const { localization } = useAuthPlugin(oauthProviderPlugin)\n\n  return (\n    <Empty>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <ShieldCheck />\n        </EmptyMedia>\n        <EmptyTitle>{localization.noConnectedApplications}</EmptyTitle>\n        <EmptyDescription>\n          {localization.connectedApplicationsDescription}\n        </EmptyDescription>\n      </EmptyHeader>\n    </Empty>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-applications-empty.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/authorized-application-skeleton.tsx",
      "content": "\"use client\"\n\nimport { Item, ItemContent, ItemMedia } from \"@/components/ui/item\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\n\nexport function AuthorizedApplicationSkeleton() {\n  return (\n    <Item>\n      <ItemMedia>\n        <Skeleton className=\"size-10 shrink-0 rounded-md\" />\n      </ItemMedia>\n      <ItemContent>\n        <Skeleton className=\"h-4 w-32\" />\n        <Skeleton className=\"h-3 w-40\" />\n\n        <div className=\"flex gap-1.5\">\n          <Skeleton className=\"h-5 w-20 rounded-full\" />\n          <Skeleton className=\"h-5 w-24 rounded-full\" />\n        </div>\n      </ItemContent>\n    </Item>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/authorized-application-skeleton.tsx"
    },
    {
      "path": "src/components/auth/oauth-provider/remove-authorization-dialog.tsx",
      "content": "\"use client\"\n\nimport type { AuthorizedOAuthApplication } from \"@better-auth-ui/core/plugins\"\nimport {\n  type OAuthProviderAuthClient,\n  useAuth,\n  useAuthPlugin,\n  useDeleteOAuthConsent\n} from \"@better-auth-ui/react\"\nimport { ShieldOff } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { oauthProviderPlugin } from \"@/lib/auth/oauth-provider-plugin\"\n\nexport type RemoveAuthorizationDialogProps = {\n  /** @remarks `AuthorizedOAuthApplication` */\n  application: AuthorizedOAuthApplication\n  clientName: string\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\n/**\n * Confirmation for removing every consent record tied to one OAuth client.\n *\n * The copy is deliberate: Better Auth's consent deletion removes the stored\n * approval, so the application must ask again — it does not revoke access or\n * refresh tokens that were already issued.\n */\nexport function RemoveAuthorizationDialog({\n  application,\n  clientName,\n  open,\n  onOpenChange\n}: RemoveAuthorizationDialogProps) {\n  const { authClient, localization } = useAuth()\n  const { localization: oauthLocalization } = useAuthPlugin(oauthProviderPlugin)\n  const [isRemoving, setIsRemoving] = useState(false)\n\n  const { mutateAsync: deleteConsent } = useDeleteOAuthConsent(\n    authClient as OAuthProviderAuthClient\n  )\n\n  const removeAuthorization = async () => {\n    setIsRemoving(true)\n\n    try {\n      // Sequential so a mid-list failure leaves a predictable server state\n      // that the refetched list reflects accurately.\n      for (const id of application.consentIds) {\n        await deleteConsent({ id })\n      }\n\n      onOpenChange(false)\n    } catch {\n      // The error toaster reports the failure; the dialog stays open so the\n      // remaining records can be retried.\n    } finally {\n      setIsRemoving(false)\n    }\n  }\n\n  return (\n    <AlertDialog open={open} onOpenChange={onOpenChange}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogMedia>\n            <ShieldOff />\n          </AlertDialogMedia>\n\n          <AlertDialogTitle>\n            {oauthLocalization.removeAuthorizationTitle}\n          </AlertDialogTitle>\n\n          <AlertDialogDescription>\n            {oauthLocalization.removeAuthorizationDescription}\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n\n        <p className=\"text-sm font-medium\">{clientName}</p>\n\n        <AlertDialogFooter>\n          <AlertDialogCancel disabled={isRemoving}>\n            {localization.settings.cancel}\n          </AlertDialogCancel>\n\n          <Button\n            type=\"button\"\n            variant=\"destructive\"\n            disabled={isRemoving}\n            onClick={removeAuthorization}\n          >\n            {isRemoving && <Spinner />}\n\n            {oauthLocalization.remove}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/oauth-provider/remove-authorization-dialog.tsx"
    }
  ],
  "type": "registry:component"
}