{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "device-authorization",
  "title": "Device Authorization",
  "description": "Device authorization plugin: code entry, sign-in continuation, and approve or deny actions for Better Auth device requests.",
  "dependencies": [
    "@better-auth-ui/react@latest",
    "@better-auth-ui/core@latest",
    "better-auth",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-otp",
    "separator",
    "spinner"
  ],
  "files": [
    {
      "path": "src/lib/auth/device-authorization-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  deviceAuthorizationPlugin as coreDeviceAuthorizationPlugin,\n  type DeviceAuthorizationPluginOptions\n} from \"@better-auth-ui/core/plugins\"\n\nimport { DeviceAuthorization } from \"@/components/auth/device-authorization/device-authorization\"\n\nexport const deviceAuthorizationPlugin = createAuthPlugin(\n  coreDeviceAuthorizationPlugin.id,\n  (options: DeviceAuthorizationPluginOptions = {}) => ({\n    ...coreDeviceAuthorizationPlugin(options),\n    views: {\n      auth: {\n        deviceAuthorization: DeviceAuthorization\n      }\n    }\n  })\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/device-authorization-plugin.ts"
    },
    {
      "path": "src/components/auth/device-authorization/device-authorization.tsx",
      "content": "\"use client\"\n\nimport type { DeviceAuthorizationLocalization } from \"@better-auth-ui/core/plugins\"\nimport {\n  type DeviceAuthorizationAuthClient,\n  useApproveDevice,\n  useAuth,\n  useAuthPlugin,\n  useDenyDevice,\n  useSession,\n  useVerifyDeviceCode\n} from \"@better-auth-ui/react\"\nimport { REGEXP_ONLY_DIGITS_AND_CHARS } from \"input-otp\"\nimport { CheckIcon, CircleCheckIcon, CircleXIcon, XIcon } from \"lucide-react\"\nimport {\n  type FormEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useReducer,\n  useRef,\n  useState\n} from \"react\"\n\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 {\n  Field,\n  FieldError,\n  FieldGroup,\n  FieldLabel\n} from \"@/components/ui/field\"\nimport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSeparator,\n  InputOTPSlot\n} from \"@/components/ui/input-otp\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { deviceAuthorizationPlugin } from \"@/lib/auth/device-authorization-plugin\"\nimport { cn } from \"@/lib/utils\"\n\ntype DeviceAuthorizationStep = \"code\" | \"approval\" | \"approved\" | \"denied\"\n\ntype DeviceAuthorizationState = {\n  step: DeviceAuthorizationStep\n  codeError: string\n}\n\ntype DeviceAuthorizationAction =\n  | { type: \"codeChanged\" }\n  | { type: \"verificationFailed\"; message: string }\n  | { type: \"verificationSucceeded\"; status: string }\n  | { type: \"approved\" }\n  | { type: \"denied\" }\n\nconst initialDeviceAuthorizationState: DeviceAuthorizationState = {\n  step: \"code\",\n  codeError: \"\"\n}\n\nfunction deviceAuthorizationReducer(\n  state: DeviceAuthorizationState,\n  action: DeviceAuthorizationAction\n): DeviceAuthorizationState {\n  switch (action.type) {\n    case \"codeChanged\":\n      return state.codeError ? { ...state, codeError: \"\" } : state\n    case \"verificationFailed\":\n      return { step: \"code\", codeError: action.message }\n    case \"verificationSucceeded\":\n      if (action.status === \"approved\") {\n        return { step: \"approved\", codeError: \"\" }\n      }\n      if (action.status === \"denied\") {\n        return { step: \"denied\", codeError: \"\" }\n      }\n      return { step: \"approval\", codeError: \"\" }\n    case \"approved\":\n      return { step: \"approved\", codeError: \"\" }\n    case \"denied\":\n      return { step: \"denied\", codeError: \"\" }\n  }\n}\n\nfunction normalizeDeviceCode(value: string) {\n  return value.replace(/-/g, \"\").trim().toUpperCase()\n}\n\nfunction createDeviceCodeSlots(length: number) {\n  return Array.from({ length }, (_, slotIndex) => ({\n    id: `device-code-character-${String(slotIndex + 1)}`,\n    index: slotIndex\n  }))\n}\n\nexport type DeviceAuthorizationProps = {\n  className?: string\n}\n\n/**\n * Render Better Auth's browser-side device authorization ceremony.\n *\n * The view accepts a user code, sends unauthenticated users through sign-in\n * with a return URL, verifies and claims the code for the current session,\n * and lets the user approve or deny the device.\n *\n * @param className - Additional CSS classes applied to the card.\n */\nexport function DeviceAuthorization({ className }: DeviceAuthorizationProps) {\n  const { authClient, basePaths, navigate, redirectTo, viewPaths } = useAuth()\n  const {\n    localization,\n    userCodeLength,\n    viewPaths: deviceAuthorizationViewPaths\n  } = useAuthPlugin(deviceAuthorizationPlugin)\n  const deviceAuthClient = authClient as DeviceAuthorizationAuthClient\n  const { data: session, isPending: isSessionPending } =\n    useSession(deviceAuthClient)\n  const [userCode, setUserCode] = useState(\"\")\n  const [state, dispatch] = useReducer(\n    deviceAuthorizationReducer,\n    initialDeviceAuthorizationState\n  )\n  const submittedCodeRef = useRef<string | null>(null)\n  const normalizedUserCode = normalizeDeviceCode(userCode)\n\n  const handleAuthorizationError = () => {\n    dispatch({\n      type: \"verificationFailed\",\n      message: localization.invalidDeviceCode\n    })\n  }\n\n  useEffect(() => {\n    const code = new URLSearchParams(window.location.search).get(\"user_code\")\n    if (!code) return\n\n    setUserCode(\n      normalizeDeviceCode(code)\n        .replace(/[^A-Z0-9]/g, \"\")\n        .slice(0, userCodeLength)\n    )\n  }, [userCodeLength])\n\n  const { mutate: verifyDeviceCode, isPending: isVerifying } =\n    useVerifyDeviceCode(deviceAuthClient, {\n      onError: handleAuthorizationError,\n      onSuccess: ({ status }) => {\n        dispatch({ type: \"verificationSucceeded\", status })\n      }\n    })\n\n  const { mutate: approveDevice, isPending: isApproving } = useApproveDevice(\n    deviceAuthClient,\n    {\n      onError: handleAuthorizationError,\n      onSuccess: () => dispatch({ type: \"approved\" })\n    }\n  )\n\n  const { mutate: denyDevice, isPending: isDenying } = useDenyDevice(\n    deviceAuthClient,\n    {\n      onError: handleAuthorizationError,\n      onSuccess: () => dispatch({ type: \"denied\" })\n    }\n  )\n\n  const handleCodeChange = (value: string) => {\n    const nextCode = normalizeDeviceCode(value)\n      .replace(/[^A-Z0-9]/g, \"\")\n      .slice(0, userCodeLength)\n\n    if (nextCode !== submittedCodeRef.current) {\n      submittedCodeRef.current = null\n    }\n\n    setUserCode(nextCode)\n    dispatch({ type: \"codeChanged\" })\n  }\n\n  const submitCode = useCallback(\n    (completedCode: string) => {\n      const normalizedCode = normalizeDeviceCode(completedCode)\n\n      if (\n        isSessionPending ||\n        isVerifying ||\n        normalizedCode.length !== userCodeLength ||\n        normalizedCode === submittedCodeRef.current\n      ) {\n        return\n      }\n\n      submittedCodeRef.current = normalizedCode\n\n      if (!session) {\n        const verificationPath = `${basePaths.auth}/${deviceAuthorizationViewPaths.auth.deviceAuthorization}?user_code=${encodeURIComponent(normalizedCode)}`\n        const signInPath = `${basePaths.auth}/${viewPaths.auth.signIn}?redirectTo=${encodeURIComponent(verificationPath)}`\n        navigate({ to: signInPath })\n        return\n      }\n\n      verifyDeviceCode({\n        query: { user_code: normalizedCode }\n      })\n    },\n    [\n      basePaths.auth,\n      deviceAuthorizationViewPaths.auth.deviceAuthorization,\n      isSessionPending,\n      isVerifying,\n      navigate,\n      session,\n      userCodeLength,\n      verifyDeviceCode,\n      viewPaths.auth.signIn\n    ]\n  )\n\n  useEffect(() => {\n    if (normalizedUserCode.length === userCodeLength) {\n      submitCode(normalizedUserCode)\n    }\n  }, [normalizedUserCode, submitCode, userCodeLength])\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault()\n\n    if (normalizedUserCode.length !== userCodeLength) {\n      handleAuthorizationError()\n      return\n    }\n\n    submitCode(normalizedUserCode)\n  }\n\n  const cardClassName = cn(\"w-full max-w-sm\", className)\n\n  if (state.step === \"approval\" && session) {\n    return (\n      <DeviceApproval\n        className={cardClassName}\n        localization={localization}\n        userCode={normalizedUserCode}\n        user={session.user}\n        isApproving={isApproving}\n        isDenying={isDenying}\n        onApprove={() => approveDevice({ userCode: normalizedUserCode })}\n        onDeny={() => denyDevice({ userCode: normalizedUserCode })}\n      />\n    )\n  }\n\n  if (state.step === \"approved\" || state.step === \"denied\") {\n    return (\n      <DeviceAuthorizationResult\n        className={cardClassName}\n        localization={localization}\n        status={state.step}\n        action={\n          <Button\n            className=\"w-full\"\n            onClick={() => navigate({ to: redirectTo })}\n          >\n            {localization.returnToApplication}\n          </Button>\n        }\n      />\n    )\n  }\n\n  return (\n    <DeviceCodeForm\n      className={cardClassName}\n      codeError={state.codeError}\n      isSessionPending={isSessionPending}\n      isVerifying={isVerifying}\n      localization={localization}\n      userCode={userCode}\n      userCodeLength={userCodeLength}\n      onCodeChange={handleCodeChange}\n      onSubmit={handleSubmit}\n    />\n  )\n}\n\ntype DeviceCodeFormProps = {\n  className: string\n  codeError: string\n  isSessionPending: boolean\n  isVerifying: boolean\n  localization: DeviceAuthorizationLocalization\n  userCode: string\n  userCodeLength: number\n  onCodeChange: (value: string) => void\n  onSubmit: (event: FormEvent<HTMLFormElement>) => void\n}\n\nfunction DeviceCodeForm({\n  className,\n  codeError,\n  isSessionPending,\n  isVerifying,\n  localization,\n  userCode,\n  userCodeLength,\n  onCodeChange,\n  onSubmit\n}: DeviceCodeFormProps) {\n  const slots = createDeviceCodeSlots(userCodeLength)\n  const groupBreak = Math.ceil(userCodeLength / 2)\n  const firstGroup = slots.slice(0, groupBreak)\n  const secondGroup = slots.slice(groupBreak)\n  const errorId = \"device-code-error\"\n\n  return (\n    <Card className={className}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">\n          {localization.deviceAuthorization}\n        </CardTitle>\n        <CardDescription>\n          {localization.deviceAuthorizationDescription}\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent>\n        <form aria-label={localization.deviceAuthorization} onSubmit={onSubmit}>\n          <FieldGroup>\n            <Field data-invalid={Boolean(codeError)}>\n              <FieldLabel htmlFor=\"device-code\">\n                {localization.deviceCode}\n              </FieldLabel>\n\n              <InputOTP\n                id=\"device-code\"\n                aria-describedby={codeError ? errorId : undefined}\n                aria-invalid={Boolean(codeError)}\n                autoComplete=\"one-time-code\"\n                containerClassName=\"w-full justify-center\"\n                disabled={isVerifying}\n                inputMode=\"text\"\n                maxLength={userCodeLength}\n                name=\"userCode\"\n                pasteTransformer={normalizeDeviceCode}\n                pattern={REGEXP_ONLY_DIGITS_AND_CHARS}\n                value={userCode}\n                onChange={onCodeChange}\n              >\n                <InputOTPGroup>\n                  {firstGroup.map((slot) => (\n                    <InputOTPSlot key={slot.id} index={slot.index} />\n                  ))}\n                </InputOTPGroup>\n\n                {secondGroup.length > 0 ? (\n                  <>\n                    <InputOTPSeparator />\n                    <InputOTPGroup>\n                      {secondGroup.map((slot) => (\n                        <InputOTPSlot key={slot.id} index={slot.index} />\n                      ))}\n                    </InputOTPGroup>\n                  </>\n                ) : null}\n              </InputOTP>\n\n              <FieldError id={errorId}>{codeError}</FieldError>\n            </Field>\n\n            <Button\n              className=\"w-full\"\n              disabled={\n                userCode.length !== userCodeLength ||\n                isSessionPending ||\n                isVerifying\n              }\n              type=\"submit\"\n            >\n              {isVerifying ? <Spinner data-icon=\"inline-start\" /> : null}\n              {localization.continue}\n            </Button>\n          </FieldGroup>\n        </form>\n      </CardContent>\n    </Card>\n  )\n}\n\ntype DeviceApprovalProps = {\n  className: string\n  isApproving: boolean\n  isDenying: boolean\n  localization: DeviceAuthorizationLocalization\n  user: {\n    email: string\n    name: string\n  }\n  userCode: string\n  onApprove: () => void\n  onDeny: () => void\n}\n\nfunction DeviceApproval({\n  className,\n  isApproving,\n  isDenying,\n  localization,\n  user,\n  userCode,\n  onApprove,\n  onDeny\n}: DeviceApprovalProps) {\n  const isPending = isApproving || isDenying\n\n  return (\n    <Card className={className}>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{localization.approveDevice}</CardTitle>\n        <CardDescription>\n          {localization.approveDeviceDescription}\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent>\n        <div className=\"flex flex-col gap-3 rounded-lg border bg-muted/50 p-3\">\n          <div className=\"flex flex-col gap-1\">\n            <p className=\"text-xs text-muted-foreground\">\n              {localization.deviceCode}\n            </p>\n            <p className=\"font-mono text-sm font-medium tracking-wider\">\n              {userCode}\n            </p>\n          </div>\n\n          <Separator />\n\n          <div className=\"flex flex-col gap-1\">\n            <p className=\"text-xs text-muted-foreground\">\n              {localization.signedInAs}\n            </p>\n            <p className=\"text-sm font-medium\">{user.name || user.email}</p>\n            {user.name ? (\n              <p className=\"text-xs text-muted-foreground\">{user.email}</p>\n            ) : null}\n          </div>\n        </div>\n      </CardContent>\n\n      <CardFooter className=\"grid grid-cols-2 gap-2\">\n        <Button disabled={isPending} variant=\"outline\" onClick={onDeny}>\n          {isDenying ? (\n            <Spinner data-icon=\"inline-start\" />\n          ) : (\n            <XIcon data-icon=\"inline-start\" />\n          )}\n          {localization.deny}\n        </Button>\n\n        <Button disabled={isPending} onClick={onApprove}>\n          {isApproving ? (\n            <Spinner data-icon=\"inline-start\" />\n          ) : (\n            <CheckIcon data-icon=\"inline-start\" />\n          )}\n          {localization.approve}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n\ntype DeviceAuthorizationResultProps = {\n  action: ReactNode\n  className: string\n  localization: DeviceAuthorizationLocalization\n  status: \"approved\" | \"denied\"\n}\n\nfunction DeviceAuthorizationResult({\n  action,\n  className,\n  localization,\n  status\n}: DeviceAuthorizationResultProps) {\n  const approved = status === \"approved\"\n  const Icon = approved ? CircleCheckIcon : CircleXIcon\n\n  return (\n    <Card className={className}>\n      <CardHeader className=\"justify-items-center text-center\">\n        <Icon\n          aria-hidden=\"true\"\n          className={cn(\n            \"mb-1 size-10\",\n            approved ? \"text-primary\" : \"text-destructive\"\n          )}\n        />\n        <CardTitle className=\"text-xl\">\n          {approved ? localization.deviceApproved : localization.deviceDenied}\n        </CardTitle>\n        <CardDescription>\n          {approved\n            ? localization.deviceApprovedDescription\n            : localization.deviceDeniedDescription}\n        </CardDescription>\n      </CardHeader>\n\n      <CardFooter>{action}</CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/device-authorization/device-authorization.tsx"
    }
  ],
  "type": "registry:component"
}