{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "action-confirmation-provider", "dependencies": [ "@inertiajs/react" ], "registryDependencies": [ "@craft/confirm-action", "button", "dialog", "input", "input-otp", "label" ], "files": [ { "path": "registry/new-york-v4/components/action-confirmation-provider.tsx", "content": "import { REGEXP_ONLY_DIGITS } from \"input-otp\";\nimport {\n createContext,\n type FormEvent,\n type ReactNode,\n useCallback,\n useContext,\n useRef,\n useState,\n} from \"react\";\nimport InputError from \"@/components/input-error\";\nimport PasswordInput from \"@/components/password-input\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from \"@/components/ui/input-otp\";\nimport { Label } from \"@/components/ui/label\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { OTP_MAX_LENGTH } from \"@/hooks/use-two-factor-auth\";\nimport { type ConfirmationType, runConfirmAction } from \"@/lib/confirm-action\";\n\ntype ConfirmSensitiveAction = (action: () => Promise) => Promise;\n\ntype ActionConfirmationContextValue = {\n confirmSensitiveAction: ConfirmSensitiveAction;\n};\n\nclass ActionConfirmationCancelledError extends Error {\n constructor() {\n super(\"Action confirmation was cancelled.\");\n this.name = \"ActionConfirmationCancelledError\";\n }\n}\n\nconst ActionConfirmationContext = createContext(null);\n\ntype PendingConfirmation = {\n resolve: () => void;\n reject: (error: Error) => void;\n};\n\nconst getCsrfHeader = (): Record => {\n const token = document.querySelector('meta[name=\"csrf-token\"]')?.content;\n\n if (token) {\n return { \"X-CSRF-TOKEN\": token };\n }\n\n const cookie = document.cookie\n .split(\"; \")\n .find((value) => value.startsWith(\"XSRF-TOKEN=\"))\n ?.slice(\"XSRF-TOKEN=\".length);\n\n return cookie ? { \"X-XSRF-TOKEN\": decodeURIComponent(cookie) } : {};\n};\n\nconst getError = async (response: Response, field: \"password\" | \"code\"): Promise => {\n try {\n const payload = await response.json();\n const fieldErrors = payload?.errors?.[field];\n\n if (Array.isArray(fieldErrors) && typeof fieldErrors[0] === \"string\") {\n return fieldErrors[0];\n }\n\n if (typeof payload?.message === \"string\") {\n return payload.message;\n }\n } catch {\n // Fall through to the generic message below.\n }\n\n return field === \"password\"\n ? \"Unable to confirm your password.\"\n : \"Unable to confirm your authentication code.\";\n};\n\nconst normalizeAuthenticationCode = (value: string): string => value.replace(/\\D/g, \"\");\n\nexport function ActionConfirmationProvider({ children }: { children: ReactNode }) {\n const pendingConfirmation = useRef(null);\n const [confirmationType, setConfirmationType] = useState(null);\n const [password, setPassword] = useState(\"\");\n const [code, setCode] = useState(\"\");\n const [error, setError] = useState(null);\n const [processing, setProcessing] = useState(false);\n\n const open = confirmationType !== null;\n const isTwoFactorConfirmation = confirmationType === \"two-factor\";\n\n const resetDialog = () => {\n setPassword(\"\");\n setCode(\"\");\n setError(null);\n setProcessing(false);\n };\n\n const requestConfirmation = useCallback((type: ConfirmationType): Promise => {\n resetDialog();\n setConfirmationType(type);\n\n return new Promise((resolve, reject) => {\n pendingConfirmation.current = { resolve, reject };\n });\n }, []);\n\n const confirmSensitiveAction = useCallback(\n (action) => runConfirmAction(action, requestConfirmation),\n [requestConfirmation],\n );\n\n const closeDialog = () => {\n pendingConfirmation.current?.reject(new ActionConfirmationCancelledError());\n pendingConfirmation.current = null;\n setConfirmationType(null);\n resetDialog();\n };\n\n const handleSubmit = async (event: FormEvent) => {\n event.preventDefault();\n\n if (!confirmationType) {\n return;\n }\n\n setProcessing(true);\n setError(null);\n\n const field = isTwoFactorConfirmation ? \"code\" : \"password\";\n const response = await fetch(\n isTwoFactorConfirmation ? \"/auth/two-factor-authentication/confirm\" : \"/confirm-password\",\n {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...getCsrfHeader(),\n },\n credentials: \"same-origin\",\n body: JSON.stringify(isTwoFactorConfirmation ? { code } : { password }),\n },\n );\n\n if (!response.ok) {\n setError(await getError(response, field));\n setProcessing(false);\n\n return;\n }\n\n pendingConfirmation.current?.resolve();\n pendingConfirmation.current = null;\n setConfirmationType(null);\n resetDialog();\n };\n\n return (\n \n {children}\n\n !isOpen && closeDialog()}>\n \n \n \n {isTwoFactorConfirmation ? \"Confirm authentication code\" : \"Confirm password\"}\n \n \n {isTwoFactorConfirmation\n ? \"Enter the authentication code from your authenticator app to continue this action.\"\n : \"Confirm your password to continue this action.\"}\n \n \n\n
\n {isTwoFactorConfirmation ? (\n
\n \n setCode(normalizeAuthenticationCode(value))}\n disabled={processing}\n pattern={REGEXP_ONLY_DIGITS}\n autoFocus\n >\n \n {Array.from({ length: OTP_MAX_LENGTH }).map((_, index) => (\n \n ))}\n \n \n \n
\n ) : (\n
\n \n setPassword(event.target.value)}\n autoComplete=\"current-password\"\n autoFocus\n required\n />\n \n
\n )}\n\n \n \n \n {processing && }\n {isTwoFactorConfirmation ? \"Confirm code\" : \"Confirm password\"}\n \n \n
\n
\n
\n
\n );\n}\n\nexport function useActionConfirmation() {\n const context = useContext(ActionConfirmationContext);\n\n if (!context) {\n throw new Error(\"useActionConfirmation must be used within ActionConfirmationProvider.\");\n }\n\n return context;\n}\n", "type": "registry:component" } ], "type": "registry:component" }