otp-auto-fetch-input 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # otp-auto-fetch-input
2
+
3
+ Animated OTP input component for React with WebOTP SMS auto-fetch support.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install otp-auto-fetch-input
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { useState } from "react";
15
+ import { OtpInput } from "otp-auto-fetch-input";
16
+ import "otp-auto-fetch-input/styles.css";
17
+
18
+ export default function VerifyOtp() {
19
+ const [otp, setOtp] = useState("");
20
+
21
+ return (
22
+ <OtpInput
23
+ length={6}
24
+ value={otp}
25
+ onChange={(next) => setOtp(next)}
26
+ onComplete={(code) => {
27
+ console.log("OTP complete:", code);
28
+ }}
29
+ autoFetch
30
+ />
31
+ );
32
+ }
33
+ ```
34
+
35
+ ## Props
36
+
37
+ - `length` (default `6`): number of OTP digits.
38
+ - `value`: controlled OTP value.
39
+ - `onChange(nextValue, source)`: triggered on manual/paste/WebOTP fill.
40
+ - `onComplete(otp, source)`: called when all digits are present.
41
+ - `autoFetch` (default `true`): enables WebOTP auto-fetch on supported browsers.
42
+ - `fetchTimeoutMs` (default `60000`): abort timeout for WebOTP listener.
43
+ - `autoFocus` (default `true`): focus first empty digit on mount/update.
44
+ - `allowPaste` (default `true`): allows full OTP paste.
45
+ - `disabled`: disable input.
46
+ - `className`: wrapper class override.
47
+ - `inputClassName`: slot class override.
48
+ - `onError(error)`: receives non-abort WebOTP errors.
49
+
50
+ ## WebOTP Notes
51
+
52
+ - Works only in secure contexts (`https`) and compatible mobile browsers.
53
+ - SMS should include your domain and OTP, for example:
54
+
55
+ ```text
56
+ Your verification code is 123456.
57
+
58
+ @example.com #123456
59
+ ```
60
+
61
+ ## Semantic Versioning
62
+
63
+ This package follows SemVer (`MAJOR.MINOR.PATCH`).
64
+
65
+ - `PATCH`: bug fixes (`0.1.0 -> 0.1.1`)
66
+ - `MINOR`: backward-compatible features (`0.1.0 -> 0.2.0`)
67
+ - `MAJOR`: breaking API changes (`0.1.0 -> 1.0.0`)
68
+
69
+ Release commands:
70
+
71
+ ```bash
72
+ npm run release:patch
73
+ npm run release:minor
74
+ npm run release:major
75
+ ```
76
+
77
+ Or let `standard-version` infer bump type from commits:
78
+
79
+ ```bash
80
+ npm run release
81
+ ```
82
+
83
+ ## Publish Pipeline (GitHub Actions)
84
+
85
+ Automation files:
86
+
87
+ - `.github/workflows/ci.yml`: validates build on pull requests and `main` pushes.
88
+ - `.github/workflows/publish.yml`: publishes to npm on `v*.*.*` tag pushes.
89
+
90
+ Required GitHub secret:
91
+
92
+ - `NPM_TOKEN`: npm automation token with publish access.
93
+
94
+ Typical release flow:
95
+
96
+ ```bash
97
+ # 1) Create version + changelog + tag (example patch)
98
+ npm run release:patch
99
+
100
+ # 2) Push commits and tags
101
+ git push --follow-tags
102
+ ```
103
+
104
+ When the `vX.Y.Z` tag reaches GitHub, publish workflow runs and pushes the package to npm.
package/dist/index.cjs ADDED
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ OtpInput: () => OtpInput,
24
+ useWebOtp: () => useWebOtp
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/OtpInput.tsx
29
+ var import_react2 = require("react");
30
+
31
+ // src/useWebOtp.ts
32
+ var import_react = require("react");
33
+ var hasWebOtp = () => {
34
+ if (typeof window === "undefined") return false;
35
+ if (!window.isSecureContext) return false;
36
+ return typeof navigator.credentials?.get === "function";
37
+ };
38
+ var useWebOtp = (enabled, timeoutMs, onCode, onError) => {
39
+ const abortRef = (0, import_react.useRef)(null);
40
+ const stop = (0, import_react.useCallback)(() => {
41
+ abortRef.current?.abort();
42
+ abortRef.current = null;
43
+ }, []);
44
+ const start = (0, import_react.useCallback)(async () => {
45
+ if (!enabled || !hasWebOtp()) return;
46
+ stop();
47
+ const controller = new AbortController();
48
+ abortRef.current = controller;
49
+ const timer = setTimeout(() => {
50
+ controller.abort();
51
+ }, timeoutMs);
52
+ try {
53
+ const credentials = navigator.credentials;
54
+ const credential = await credentials.get?.({
55
+ otp: { transport: ["sms"] },
56
+ signal: controller.signal
57
+ });
58
+ if (credential?.code) {
59
+ onCode({ code: credential.code, source: "webotp" });
60
+ }
61
+ } catch (err) {
62
+ const error = err;
63
+ if (error.name !== "AbortError") {
64
+ onError?.(error);
65
+ }
66
+ } finally {
67
+ clearTimeout(timer);
68
+ abortRef.current = null;
69
+ }
70
+ }, [enabled, onCode, onError, stop, timeoutMs]);
71
+ (0, import_react.useEffect)(() => {
72
+ if (!enabled) return;
73
+ void start();
74
+ return () => {
75
+ stop();
76
+ };
77
+ }, [enabled, start, stop]);
78
+ return {
79
+ isSupported: hasWebOtp(),
80
+ start,
81
+ stop
82
+ };
83
+ };
84
+
85
+ // src/OtpInput.tsx
86
+ var import_jsx_runtime = require("react/jsx-runtime");
87
+ var isDigit = (value) => /^\d$/.test(value);
88
+ var toSlots = (value, length) => {
89
+ const normalized = value.replace(/\D/g, "").slice(0, length);
90
+ return Array.from({ length }, (_, idx) => normalized[idx] ?? "");
91
+ };
92
+ var OtpInput = ({
93
+ length = 6,
94
+ value,
95
+ onChange,
96
+ onComplete,
97
+ autoFocus = true,
98
+ disabled = false,
99
+ autoFetch = true,
100
+ fetchTimeoutMs = 6e4,
101
+ className,
102
+ inputClassName,
103
+ allowPaste = true,
104
+ onError
105
+ }) => {
106
+ const [enteredIndex, setEnteredIndex] = (0, import_react2.useState)(null);
107
+ const refs = (0, import_react2.useRef)([]);
108
+ const slots = (0, import_react2.useMemo)(() => toSlots(value, length), [value, length]);
109
+ const emitValue = (0, import_react2.useCallback)(
110
+ (next, source) => {
111
+ const trimmed = next.replace(/\D/g, "").slice(0, length);
112
+ onChange(trimmed, source);
113
+ if (trimmed.length === length) {
114
+ onComplete?.(trimmed, source);
115
+ }
116
+ },
117
+ [length, onChange, onComplete]
118
+ );
119
+ useWebOtp(
120
+ autoFetch,
121
+ fetchTimeoutMs,
122
+ ({ code, source }) => {
123
+ emitValue(code, source);
124
+ refs.current[Math.min(code.length, length) - 1]?.blur();
125
+ },
126
+ onError
127
+ );
128
+ (0, import_react2.useEffect)(() => {
129
+ if (!autoFocus || disabled) return;
130
+ const firstEmpty = slots.findIndex((slot) => slot === "");
131
+ const targetIndex = firstEmpty === -1 ? length - 1 : firstEmpty;
132
+ refs.current[targetIndex]?.focus();
133
+ }, [autoFocus, disabled, length, slots]);
134
+ (0, import_react2.useEffect)(() => {
135
+ if (enteredIndex === null) return;
136
+ const timer = setTimeout(() => setEnteredIndex(null), 220);
137
+ return () => clearTimeout(timer);
138
+ }, [enteredIndex]);
139
+ const handleChangeAt = (index, raw) => {
140
+ if (disabled) return;
141
+ const nextChar = raw.slice(-1);
142
+ if (!isDigit(nextChar)) return;
143
+ const nextSlots = [...slots];
144
+ nextSlots[index] = nextChar;
145
+ const nextValue = nextSlots.join("");
146
+ setEnteredIndex(index);
147
+ emitValue(nextValue, "manual");
148
+ const nextIndex = Math.min(index + 1, length - 1);
149
+ refs.current[nextIndex]?.focus();
150
+ refs.current[nextIndex]?.select();
151
+ };
152
+ const clearAt = (index) => {
153
+ const nextSlots = [...slots];
154
+ nextSlots[index] = "";
155
+ emitValue(nextSlots.join(""), "manual");
156
+ };
157
+ const handleKeyDown = (index, event) => {
158
+ if (disabled) return;
159
+ if (event.key === "Backspace") {
160
+ event.preventDefault();
161
+ if (slots[index]) {
162
+ clearAt(index);
163
+ return;
164
+ }
165
+ const prev = Math.max(index - 1, 0);
166
+ clearAt(prev);
167
+ refs.current[prev]?.focus();
168
+ return;
169
+ }
170
+ if (event.key === "ArrowLeft") {
171
+ event.preventDefault();
172
+ refs.current[Math.max(index - 1, 0)]?.focus();
173
+ return;
174
+ }
175
+ if (event.key === "ArrowRight") {
176
+ event.preventDefault();
177
+ refs.current[Math.min(index + 1, length - 1)]?.focus();
178
+ return;
179
+ }
180
+ if (event.key.length === 1 && !/\d/.test(event.key)) {
181
+ event.preventDefault();
182
+ }
183
+ };
184
+ const handlePaste = (event) => {
185
+ if (!allowPaste || disabled) return;
186
+ event.preventDefault();
187
+ const text = event.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
188
+ if (!text) return;
189
+ emitValue(text, "paste");
190
+ setEnteredIndex(Math.min(text.length - 1, length - 1));
191
+ const focusIndex = Math.min(text.length, length - 1);
192
+ refs.current[focusIndex]?.focus();
193
+ };
194
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: ["otp-root", className].filter(Boolean).join(" "), children: slots.map((digit, index) => {
195
+ const enteredClass = enteredIndex === index ? "entered" : "";
196
+ const filledClass = digit ? "filled" : "";
197
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
198
+ "input",
199
+ {
200
+ ref: (el) => {
201
+ refs.current[index] = el;
202
+ },
203
+ value: digit,
204
+ disabled,
205
+ type: "text",
206
+ maxLength: 1,
207
+ inputMode: "numeric",
208
+ pattern: "[0-9]*",
209
+ autoComplete: index === 0 ? "one-time-code" : "off",
210
+ className: ["otp-slot", filledClass, enteredClass, inputClassName].filter(Boolean).join(" "),
211
+ onChange: (e) => handleChangeAt(index, e.target.value),
212
+ onKeyDown: (e) => handleKeyDown(index, e),
213
+ onPaste: handlePaste,
214
+ "aria-label": `OTP digit ${index + 1}`
215
+ },
216
+ index
217
+ );
218
+ }) });
219
+ };
220
+ // Annotate the CommonJS export names for ESM import in node:
221
+ 0 && (module.exports = {
222
+ OtpInput,
223
+ useWebOtp
224
+ });
225
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/OtpInput.tsx","../src/useWebOtp.ts"],"sourcesContent":["export { OtpInput } from \"./OtpInput\";\nexport { useWebOtp } from \"./useWebOtp\";\nexport type { OtpInputProps, OtpAutoFillSource, WebOtpResult } from \"./types\";\n","import React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useWebOtp } from \"./useWebOtp\";\nimport type { OtpAutoFillSource, OtpInputProps } from \"./types\";\n\nconst isDigit = (value: string) => /^\\d$/.test(value);\n\nconst toSlots = (value: string, length: number) => {\n const normalized = value.replace(/\\D/g, \"\").slice(0, length);\n return Array.from({ length }, (_, idx) => normalized[idx] ?? \"\");\n};\n\nexport const OtpInput = ({\n length = 6,\n value,\n onChange,\n onComplete,\n autoFocus = true,\n disabled = false,\n autoFetch = true,\n fetchTimeoutMs = 60_000,\n className,\n inputClassName,\n allowPaste = true,\n onError,\n}: OtpInputProps) => {\n const [enteredIndex, setEnteredIndex] = useState<number | null>(null);\n const refs = useRef<Array<HTMLInputElement | null>>([]);\n\n const slots = useMemo(() => toSlots(value, length), [value, length]);\n\n const emitValue = useCallback(\n (next: string, source: OtpAutoFillSource) => {\n const trimmed = next.replace(/\\D/g, \"\").slice(0, length);\n onChange(trimmed, source);\n if (trimmed.length === length) {\n onComplete?.(trimmed, source);\n }\n },\n [length, onChange, onComplete],\n );\n\n useWebOtp(\n autoFetch,\n fetchTimeoutMs,\n ({ code, source }) => {\n emitValue(code, source);\n refs.current[Math.min(code.length, length) - 1]?.blur();\n },\n onError,\n );\n\n useEffect(() => {\n if (!autoFocus || disabled) return;\n const firstEmpty = slots.findIndex((slot) => slot === \"\");\n const targetIndex = firstEmpty === -1 ? length - 1 : firstEmpty;\n refs.current[targetIndex]?.focus();\n }, [autoFocus, disabled, length, slots]);\n\n useEffect(() => {\n if (enteredIndex === null) return;\n const timer = setTimeout(() => setEnteredIndex(null), 220);\n return () => clearTimeout(timer);\n }, [enteredIndex]);\n\n const handleChangeAt = (index: number, raw: string) => {\n if (disabled) return;\n const nextChar = raw.slice(-1);\n if (!isDigit(nextChar)) return;\n\n const nextSlots = [...slots];\n nextSlots[index] = nextChar;\n const nextValue = nextSlots.join(\"\");\n\n setEnteredIndex(index);\n emitValue(nextValue, \"manual\");\n\n const nextIndex = Math.min(index + 1, length - 1);\n refs.current[nextIndex]?.focus();\n refs.current[nextIndex]?.select();\n };\n\n const clearAt = (index: number) => {\n const nextSlots = [...slots];\n nextSlots[index] = \"\";\n emitValue(nextSlots.join(\"\"), \"manual\");\n };\n\n const handleKeyDown = (index: number, event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return;\n\n if (event.key === \"Backspace\") {\n event.preventDefault();\n if (slots[index]) {\n clearAt(index);\n return;\n }\n const prev = Math.max(index - 1, 0);\n clearAt(prev);\n refs.current[prev]?.focus();\n return;\n }\n\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n refs.current[Math.max(index - 1, 0)]?.focus();\n return;\n }\n\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n refs.current[Math.min(index + 1, length - 1)]?.focus();\n return;\n }\n\n if (event.key.length === 1 && !/\\d/.test(event.key)) {\n event.preventDefault();\n }\n };\n\n const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {\n if (!allowPaste || disabled) return;\n event.preventDefault();\n\n const text = event.clipboardData.getData(\"text\").replace(/\\D/g, \"\").slice(0, length);\n if (!text) return;\n\n emitValue(text, \"paste\");\n setEnteredIndex(Math.min(text.length - 1, length - 1));\n\n const focusIndex = Math.min(text.length, length - 1);\n refs.current[focusIndex]?.focus();\n };\n\n return (\n <div className={[\"otp-root\", className].filter(Boolean).join(\" \")}>\n {slots.map((digit, index) => {\n const enteredClass = enteredIndex === index ? \"entered\" : \"\";\n const filledClass = digit ? \"filled\" : \"\";\n return (\n <input\n key={index}\n ref={(el) => {\n refs.current[index] = el;\n }}\n value={digit}\n disabled={disabled}\n type=\"text\"\n maxLength={1}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n className={[\"otp-slot\", filledClass, enteredClass, inputClassName].filter(Boolean).join(\" \")}\n onChange={(e) => handleChangeAt(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={handlePaste}\n aria-label={`OTP digit ${index + 1}`}\n />\n );\n })}\n </div>\n );\n};\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type { WebOtpResult } from \"./types\";\n\ninterface OtpCredential extends Credential {\n code: string;\n}\n\ntype OtpGetOptions = CredentialRequestOptions & {\n otp?: { transport: string[] };\n};\n\ntype OtpAwareCredentialsContainer = CredentialsContainer & {\n get: (options?: OtpGetOptions) => Promise<Credential | null>;\n};\n\nconst hasWebOtp = () => {\n if (typeof window === \"undefined\") return false;\n if (!window.isSecureContext) return false;\n return typeof (navigator.credentials as Partial<OtpAwareCredentialsContainer> | undefined)?.get === \"function\";\n};\n\nexport const useWebOtp = (\n enabled: boolean,\n timeoutMs: number,\n onCode: (result: WebOtpResult) => void,\n onError?: (error: Error) => void,\n) => {\n const abortRef = useRef<AbortController | null>(null);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n }, []);\n\n const start = useCallback(async () => {\n if (!enabled || !hasWebOtp()) return;\n\n stop();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const timer = setTimeout(() => {\n controller.abort();\n }, timeoutMs);\n\n try {\n const credentials = navigator.credentials as OtpAwareCredentialsContainer;\n const credential = (await credentials.get?.({\n otp: { transport: [\"sms\"] },\n signal: controller.signal,\n })) as OtpCredential | null;\n\n if (credential?.code) {\n onCode({ code: credential.code, source: \"webotp\" });\n }\n } catch (err) {\n const error = err as Error;\n if (error.name !== \"AbortError\") {\n onError?.(error);\n }\n } finally {\n clearTimeout(timer);\n abortRef.current = null;\n }\n }, [enabled, onCode, onError, stop, timeoutMs]);\n\n useEffect(() => {\n if (!enabled) return;\n void start();\n return () => {\n stop();\n };\n }, [enabled, start, stop]);\n\n return {\n isSupported: hasWebOtp(),\n start,\n stop,\n };\n};\n"],"mappings":"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","names":["import_react"]}
@@ -0,0 +1,31 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type OtpAutoFillSource = "manual" | "paste" | "webotp";
4
+ interface OtpInputProps {
5
+ length?: number;
6
+ value: string;
7
+ onChange: (nextValue: string, source?: OtpAutoFillSource) => void;
8
+ onComplete?: (otp: string, source?: OtpAutoFillSource) => void;
9
+ autoFocus?: boolean;
10
+ disabled?: boolean;
11
+ autoFetch?: boolean;
12
+ fetchTimeoutMs?: number;
13
+ className?: string;
14
+ inputClassName?: string;
15
+ allowPaste?: boolean;
16
+ onError?: (error: Error) => void;
17
+ }
18
+ interface WebOtpResult {
19
+ code: string;
20
+ source: OtpAutoFillSource;
21
+ }
22
+
23
+ declare const OtpInput: ({ length, value, onChange, onComplete, autoFocus, disabled, autoFetch, fetchTimeoutMs, className, inputClassName, allowPaste, onError, }: OtpInputProps) => react_jsx_runtime.JSX.Element;
24
+
25
+ declare const useWebOtp: (enabled: boolean, timeoutMs: number, onCode: (result: WebOtpResult) => void, onError?: (error: Error) => void) => {
26
+ isSupported: boolean;
27
+ start: () => Promise<void>;
28
+ stop: () => void;
29
+ };
30
+
31
+ export { type OtpAutoFillSource, OtpInput, type OtpInputProps, type WebOtpResult, useWebOtp };
@@ -0,0 +1,31 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type OtpAutoFillSource = "manual" | "paste" | "webotp";
4
+ interface OtpInputProps {
5
+ length?: number;
6
+ value: string;
7
+ onChange: (nextValue: string, source?: OtpAutoFillSource) => void;
8
+ onComplete?: (otp: string, source?: OtpAutoFillSource) => void;
9
+ autoFocus?: boolean;
10
+ disabled?: boolean;
11
+ autoFetch?: boolean;
12
+ fetchTimeoutMs?: number;
13
+ className?: string;
14
+ inputClassName?: string;
15
+ allowPaste?: boolean;
16
+ onError?: (error: Error) => void;
17
+ }
18
+ interface WebOtpResult {
19
+ code: string;
20
+ source: OtpAutoFillSource;
21
+ }
22
+
23
+ declare const OtpInput: ({ length, value, onChange, onComplete, autoFocus, disabled, autoFetch, fetchTimeoutMs, className, inputClassName, allowPaste, onError, }: OtpInputProps) => react_jsx_runtime.JSX.Element;
24
+
25
+ declare const useWebOtp: (enabled: boolean, timeoutMs: number, onCode: (result: WebOtpResult) => void, onError?: (error: Error) => void) => {
26
+ isSupported: boolean;
27
+ start: () => Promise<void>;
28
+ stop: () => void;
29
+ };
30
+
31
+ export { type OtpAutoFillSource, OtpInput, type OtpInputProps, type WebOtpResult, useWebOtp };
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ // src/OtpInput.tsx
2
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
3
+
4
+ // src/useWebOtp.ts
5
+ import { useCallback, useEffect, useRef } from "react";
6
+ var hasWebOtp = () => {
7
+ if (typeof window === "undefined") return false;
8
+ if (!window.isSecureContext) return false;
9
+ return typeof navigator.credentials?.get === "function";
10
+ };
11
+ var useWebOtp = (enabled, timeoutMs, onCode, onError) => {
12
+ const abortRef = useRef(null);
13
+ const stop = useCallback(() => {
14
+ abortRef.current?.abort();
15
+ abortRef.current = null;
16
+ }, []);
17
+ const start = useCallback(async () => {
18
+ if (!enabled || !hasWebOtp()) return;
19
+ stop();
20
+ const controller = new AbortController();
21
+ abortRef.current = controller;
22
+ const timer = setTimeout(() => {
23
+ controller.abort();
24
+ }, timeoutMs);
25
+ try {
26
+ const credentials = navigator.credentials;
27
+ const credential = await credentials.get?.({
28
+ otp: { transport: ["sms"] },
29
+ signal: controller.signal
30
+ });
31
+ if (credential?.code) {
32
+ onCode({ code: credential.code, source: "webotp" });
33
+ }
34
+ } catch (err) {
35
+ const error = err;
36
+ if (error.name !== "AbortError") {
37
+ onError?.(error);
38
+ }
39
+ } finally {
40
+ clearTimeout(timer);
41
+ abortRef.current = null;
42
+ }
43
+ }, [enabled, onCode, onError, stop, timeoutMs]);
44
+ useEffect(() => {
45
+ if (!enabled) return;
46
+ void start();
47
+ return () => {
48
+ stop();
49
+ };
50
+ }, [enabled, start, stop]);
51
+ return {
52
+ isSupported: hasWebOtp(),
53
+ start,
54
+ stop
55
+ };
56
+ };
57
+
58
+ // src/OtpInput.tsx
59
+ import { jsx } from "react/jsx-runtime";
60
+ var isDigit = (value) => /^\d$/.test(value);
61
+ var toSlots = (value, length) => {
62
+ const normalized = value.replace(/\D/g, "").slice(0, length);
63
+ return Array.from({ length }, (_, idx) => normalized[idx] ?? "");
64
+ };
65
+ var OtpInput = ({
66
+ length = 6,
67
+ value,
68
+ onChange,
69
+ onComplete,
70
+ autoFocus = true,
71
+ disabled = false,
72
+ autoFetch = true,
73
+ fetchTimeoutMs = 6e4,
74
+ className,
75
+ inputClassName,
76
+ allowPaste = true,
77
+ onError
78
+ }) => {
79
+ const [enteredIndex, setEnteredIndex] = useState(null);
80
+ const refs = useRef2([]);
81
+ const slots = useMemo(() => toSlots(value, length), [value, length]);
82
+ const emitValue = useCallback2(
83
+ (next, source) => {
84
+ const trimmed = next.replace(/\D/g, "").slice(0, length);
85
+ onChange(trimmed, source);
86
+ if (trimmed.length === length) {
87
+ onComplete?.(trimmed, source);
88
+ }
89
+ },
90
+ [length, onChange, onComplete]
91
+ );
92
+ useWebOtp(
93
+ autoFetch,
94
+ fetchTimeoutMs,
95
+ ({ code, source }) => {
96
+ emitValue(code, source);
97
+ refs.current[Math.min(code.length, length) - 1]?.blur();
98
+ },
99
+ onError
100
+ );
101
+ useEffect2(() => {
102
+ if (!autoFocus || disabled) return;
103
+ const firstEmpty = slots.findIndex((slot) => slot === "");
104
+ const targetIndex = firstEmpty === -1 ? length - 1 : firstEmpty;
105
+ refs.current[targetIndex]?.focus();
106
+ }, [autoFocus, disabled, length, slots]);
107
+ useEffect2(() => {
108
+ if (enteredIndex === null) return;
109
+ const timer = setTimeout(() => setEnteredIndex(null), 220);
110
+ return () => clearTimeout(timer);
111
+ }, [enteredIndex]);
112
+ const handleChangeAt = (index, raw) => {
113
+ if (disabled) return;
114
+ const nextChar = raw.slice(-1);
115
+ if (!isDigit(nextChar)) return;
116
+ const nextSlots = [...slots];
117
+ nextSlots[index] = nextChar;
118
+ const nextValue = nextSlots.join("");
119
+ setEnteredIndex(index);
120
+ emitValue(nextValue, "manual");
121
+ const nextIndex = Math.min(index + 1, length - 1);
122
+ refs.current[nextIndex]?.focus();
123
+ refs.current[nextIndex]?.select();
124
+ };
125
+ const clearAt = (index) => {
126
+ const nextSlots = [...slots];
127
+ nextSlots[index] = "";
128
+ emitValue(nextSlots.join(""), "manual");
129
+ };
130
+ const handleKeyDown = (index, event) => {
131
+ if (disabled) return;
132
+ if (event.key === "Backspace") {
133
+ event.preventDefault();
134
+ if (slots[index]) {
135
+ clearAt(index);
136
+ return;
137
+ }
138
+ const prev = Math.max(index - 1, 0);
139
+ clearAt(prev);
140
+ refs.current[prev]?.focus();
141
+ return;
142
+ }
143
+ if (event.key === "ArrowLeft") {
144
+ event.preventDefault();
145
+ refs.current[Math.max(index - 1, 0)]?.focus();
146
+ return;
147
+ }
148
+ if (event.key === "ArrowRight") {
149
+ event.preventDefault();
150
+ refs.current[Math.min(index + 1, length - 1)]?.focus();
151
+ return;
152
+ }
153
+ if (event.key.length === 1 && !/\d/.test(event.key)) {
154
+ event.preventDefault();
155
+ }
156
+ };
157
+ const handlePaste = (event) => {
158
+ if (!allowPaste || disabled) return;
159
+ event.preventDefault();
160
+ const text = event.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
161
+ if (!text) return;
162
+ emitValue(text, "paste");
163
+ setEnteredIndex(Math.min(text.length - 1, length - 1));
164
+ const focusIndex = Math.min(text.length, length - 1);
165
+ refs.current[focusIndex]?.focus();
166
+ };
167
+ return /* @__PURE__ */ jsx("div", { className: ["otp-root", className].filter(Boolean).join(" "), children: slots.map((digit, index) => {
168
+ const enteredClass = enteredIndex === index ? "entered" : "";
169
+ const filledClass = digit ? "filled" : "";
170
+ return /* @__PURE__ */ jsx(
171
+ "input",
172
+ {
173
+ ref: (el) => {
174
+ refs.current[index] = el;
175
+ },
176
+ value: digit,
177
+ disabled,
178
+ type: "text",
179
+ maxLength: 1,
180
+ inputMode: "numeric",
181
+ pattern: "[0-9]*",
182
+ autoComplete: index === 0 ? "one-time-code" : "off",
183
+ className: ["otp-slot", filledClass, enteredClass, inputClassName].filter(Boolean).join(" "),
184
+ onChange: (e) => handleChangeAt(index, e.target.value),
185
+ onKeyDown: (e) => handleKeyDown(index, e),
186
+ onPaste: handlePaste,
187
+ "aria-label": `OTP digit ${index + 1}`
188
+ },
189
+ index
190
+ );
191
+ }) });
192
+ };
193
+ export {
194
+ OtpInput,
195
+ useWebOtp
196
+ };
197
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/OtpInput.tsx","../src/useWebOtp.ts"],"sourcesContent":["import React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useWebOtp } from \"./useWebOtp\";\nimport type { OtpAutoFillSource, OtpInputProps } from \"./types\";\n\nconst isDigit = (value: string) => /^\\d$/.test(value);\n\nconst toSlots = (value: string, length: number) => {\n const normalized = value.replace(/\\D/g, \"\").slice(0, length);\n return Array.from({ length }, (_, idx) => normalized[idx] ?? \"\");\n};\n\nexport const OtpInput = ({\n length = 6,\n value,\n onChange,\n onComplete,\n autoFocus = true,\n disabled = false,\n autoFetch = true,\n fetchTimeoutMs = 60_000,\n className,\n inputClassName,\n allowPaste = true,\n onError,\n}: OtpInputProps) => {\n const [enteredIndex, setEnteredIndex] = useState<number | null>(null);\n const refs = useRef<Array<HTMLInputElement | null>>([]);\n\n const slots = useMemo(() => toSlots(value, length), [value, length]);\n\n const emitValue = useCallback(\n (next: string, source: OtpAutoFillSource) => {\n const trimmed = next.replace(/\\D/g, \"\").slice(0, length);\n onChange(trimmed, source);\n if (trimmed.length === length) {\n onComplete?.(trimmed, source);\n }\n },\n [length, onChange, onComplete],\n );\n\n useWebOtp(\n autoFetch,\n fetchTimeoutMs,\n ({ code, source }) => {\n emitValue(code, source);\n refs.current[Math.min(code.length, length) - 1]?.blur();\n },\n onError,\n );\n\n useEffect(() => {\n if (!autoFocus || disabled) return;\n const firstEmpty = slots.findIndex((slot) => slot === \"\");\n const targetIndex = firstEmpty === -1 ? length - 1 : firstEmpty;\n refs.current[targetIndex]?.focus();\n }, [autoFocus, disabled, length, slots]);\n\n useEffect(() => {\n if (enteredIndex === null) return;\n const timer = setTimeout(() => setEnteredIndex(null), 220);\n return () => clearTimeout(timer);\n }, [enteredIndex]);\n\n const handleChangeAt = (index: number, raw: string) => {\n if (disabled) return;\n const nextChar = raw.slice(-1);\n if (!isDigit(nextChar)) return;\n\n const nextSlots = [...slots];\n nextSlots[index] = nextChar;\n const nextValue = nextSlots.join(\"\");\n\n setEnteredIndex(index);\n emitValue(nextValue, \"manual\");\n\n const nextIndex = Math.min(index + 1, length - 1);\n refs.current[nextIndex]?.focus();\n refs.current[nextIndex]?.select();\n };\n\n const clearAt = (index: number) => {\n const nextSlots = [...slots];\n nextSlots[index] = \"\";\n emitValue(nextSlots.join(\"\"), \"manual\");\n };\n\n const handleKeyDown = (index: number, event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return;\n\n if (event.key === \"Backspace\") {\n event.preventDefault();\n if (slots[index]) {\n clearAt(index);\n return;\n }\n const prev = Math.max(index - 1, 0);\n clearAt(prev);\n refs.current[prev]?.focus();\n return;\n }\n\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n refs.current[Math.max(index - 1, 0)]?.focus();\n return;\n }\n\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n refs.current[Math.min(index + 1, length - 1)]?.focus();\n return;\n }\n\n if (event.key.length === 1 && !/\\d/.test(event.key)) {\n event.preventDefault();\n }\n };\n\n const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {\n if (!allowPaste || disabled) return;\n event.preventDefault();\n\n const text = event.clipboardData.getData(\"text\").replace(/\\D/g, \"\").slice(0, length);\n if (!text) return;\n\n emitValue(text, \"paste\");\n setEnteredIndex(Math.min(text.length - 1, length - 1));\n\n const focusIndex = Math.min(text.length, length - 1);\n refs.current[focusIndex]?.focus();\n };\n\n return (\n <div className={[\"otp-root\", className].filter(Boolean).join(\" \")}>\n {slots.map((digit, index) => {\n const enteredClass = enteredIndex === index ? \"entered\" : \"\";\n const filledClass = digit ? \"filled\" : \"\";\n return (\n <input\n key={index}\n ref={(el) => {\n refs.current[index] = el;\n }}\n value={digit}\n disabled={disabled}\n type=\"text\"\n maxLength={1}\n inputMode=\"numeric\"\n pattern=\"[0-9]*\"\n autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n className={[\"otp-slot\", filledClass, enteredClass, inputClassName].filter(Boolean).join(\" \")}\n onChange={(e) => handleChangeAt(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={handlePaste}\n aria-label={`OTP digit ${index + 1}`}\n />\n );\n })}\n </div>\n );\n};\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type { WebOtpResult } from \"./types\";\n\ninterface OtpCredential extends Credential {\n code: string;\n}\n\ntype OtpGetOptions = CredentialRequestOptions & {\n otp?: { transport: string[] };\n};\n\ntype OtpAwareCredentialsContainer = CredentialsContainer & {\n get: (options?: OtpGetOptions) => Promise<Credential | null>;\n};\n\nconst hasWebOtp = () => {\n if (typeof window === \"undefined\") return false;\n if (!window.isSecureContext) return false;\n return typeof (navigator.credentials as Partial<OtpAwareCredentialsContainer> | undefined)?.get === \"function\";\n};\n\nexport const useWebOtp = (\n enabled: boolean,\n timeoutMs: number,\n onCode: (result: WebOtpResult) => void,\n onError?: (error: Error) => void,\n) => {\n const abortRef = useRef<AbortController | null>(null);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n }, []);\n\n const start = useCallback(async () => {\n if (!enabled || !hasWebOtp()) return;\n\n stop();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const timer = setTimeout(() => {\n controller.abort();\n }, timeoutMs);\n\n try {\n const credentials = navigator.credentials as OtpAwareCredentialsContainer;\n const credential = (await credentials.get?.({\n otp: { transport: [\"sms\"] },\n signal: controller.signal,\n })) as OtpCredential | null;\n\n if (credential?.code) {\n onCode({ code: credential.code, source: \"webotp\" });\n }\n } catch (err) {\n const error = err as Error;\n if (error.name !== \"AbortError\") {\n onError?.(error);\n }\n } finally {\n clearTimeout(timer);\n abortRef.current = null;\n }\n }, [enabled, onCode, onError, stop, timeoutMs]);\n\n useEffect(() => {\n if (!enabled) return;\n void start();\n return () => {\n stop();\n };\n }, [enabled, start, stop]);\n\n return {\n isSupported: hasWebOtp(),\n start,\n stop,\n };\n};\n"],"mappings":";AAAA,SAAgB,eAAAA,cAAa,aAAAC,YAAW,SAAS,UAAAC,SAAQ,gBAAgB;;;ACAzE,SAAS,aAAa,WAAW,cAAc;AAe/C,IAAM,YAAY,MAAM;AACtB,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI,CAAC,OAAO,gBAAiB,QAAO;AACpC,SAAO,OAAQ,UAAU,aAAmE,QAAQ;AACtG;AAEO,IAAM,YAAY,CACvB,SACA,WACA,QACA,YACG;AACH,QAAM,WAAW,OAA+B,IAAI;AAEpD,QAAM,OAAO,YAAY,MAAM;AAC7B,aAAS,SAAS,MAAM;AACxB,aAAS,UAAU;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,YAAY,YAAY;AACpC,QAAI,CAAC,WAAW,CAAC,UAAU,EAAG;AAE9B,SAAK;AACL,UAAM,aAAa,IAAI,gBAAgB;AACvC,aAAS,UAAU;AAEnB,UAAM,QAAQ,WAAW,MAAM;AAC7B,iBAAW,MAAM;AAAA,IACnB,GAAG,SAAS;AAEZ,QAAI;AACF,YAAM,cAAc,UAAU;AAC9B,YAAM,aAAc,MAAM,YAAY,MAAM;AAAA,QAC1C,KAAK,EAAE,WAAW,CAAC,KAAK,EAAE;AAAA,QAC1B,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,UAAI,YAAY,MAAM;AACpB,eAAO,EAAE,MAAM,WAAW,MAAM,QAAQ,SAAS,CAAC;AAAA,MACpD;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,QAAQ;AACd,UAAI,MAAM,SAAS,cAAc;AAC/B,kBAAU,KAAK;AAAA,MACjB;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAClB,eAAS,UAAU;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,SAAS,QAAQ,SAAS,MAAM,SAAS,CAAC;AAE9C,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,SAAK,MAAM;AACX,WAAO,MAAM;AACX,WAAK;AAAA,IACP;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,IAAI,CAAC;AAEzB,SAAO;AAAA,IACL,aAAa,UAAU;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AACF;;;AD4DU;AAvIV,IAAM,UAAU,CAAC,UAAkB,OAAO,KAAK,KAAK;AAEpD,IAAM,UAAU,CAAC,OAAe,WAAmB;AACjD,QAAM,aAAa,MAAM,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,MAAM;AAC3D,SAAO,MAAM,KAAK,EAAE,OAAO,GAAG,CAAC,GAAG,QAAQ,WAAW,GAAG,KAAK,EAAE;AACjE;AAEO,IAAM,WAAW,CAAC;AAAA,EACvB,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AACF,MAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAwB,IAAI;AACpE,QAAM,OAAOC,QAAuC,CAAC,CAAC;AAEtD,QAAM,QAAQ,QAAQ,MAAM,QAAQ,OAAO,MAAM,GAAG,CAAC,OAAO,MAAM,CAAC;AAEnE,QAAM,YAAYC;AAAA,IAChB,CAAC,MAAc,WAA8B;AAC3C,YAAM,UAAU,KAAK,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,MAAM;AACvD,eAAS,SAAS,MAAM;AACxB,UAAI,QAAQ,WAAW,QAAQ;AAC7B,qBAAa,SAAS,MAAM;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,UAAU,UAAU;AAAA,EAC/B;AAEA;AAAA,IACE;AAAA,IACA;AAAA,IACA,CAAC,EAAE,MAAM,OAAO,MAAM;AACpB,gBAAU,MAAM,MAAM;AACtB,WAAK,QAAQ,KAAK,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC,GAAG,KAAK;AAAA,IACxD;AAAA,IACA;AAAA,EACF;AAEA,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,aAAa,SAAU;AAC5B,UAAM,aAAa,MAAM,UAAU,CAAC,SAAS,SAAS,EAAE;AACxD,UAAM,cAAc,eAAe,KAAK,SAAS,IAAI;AACrD,SAAK,QAAQ,WAAW,GAAG,MAAM;AAAA,EACnC,GAAG,CAAC,WAAW,UAAU,QAAQ,KAAK,CAAC;AAEvC,EAAAA,WAAU,MAAM;AACd,QAAI,iBAAiB,KAAM;AAC3B,UAAM,QAAQ,WAAW,MAAM,gBAAgB,IAAI,GAAG,GAAG;AACzD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,iBAAiB,CAAC,OAAe,QAAgB;AACrD,QAAI,SAAU;AACd,UAAM,WAAW,IAAI,MAAM,EAAE;AAC7B,QAAI,CAAC,QAAQ,QAAQ,EAAG;AAExB,UAAM,YAAY,CAAC,GAAG,KAAK;AAC3B,cAAU,KAAK,IAAI;AACnB,UAAM,YAAY,UAAU,KAAK,EAAE;AAEnC,oBAAgB,KAAK;AACrB,cAAU,WAAW,QAAQ;AAE7B,UAAM,YAAY,KAAK,IAAI,QAAQ,GAAG,SAAS,CAAC;AAChD,SAAK,QAAQ,SAAS,GAAG,MAAM;AAC/B,SAAK,QAAQ,SAAS,GAAG,OAAO;AAAA,EAClC;AAEA,QAAM,UAAU,CAAC,UAAkB;AACjC,UAAM,YAAY,CAAC,GAAG,KAAK;AAC3B,cAAU,KAAK,IAAI;AACnB,cAAU,UAAU,KAAK,EAAE,GAAG,QAAQ;AAAA,EACxC;AAEA,QAAM,gBAAgB,CAAC,OAAe,UAAiD;AACrF,QAAI,SAAU;AAEd,QAAI,MAAM,QAAQ,aAAa;AAC7B,YAAM,eAAe;AACrB,UAAI,MAAM,KAAK,GAAG;AAChB,gBAAQ,KAAK;AACb;AAAA,MACF;AACA,YAAM,OAAO,KAAK,IAAI,QAAQ,GAAG,CAAC;AAClC,cAAQ,IAAI;AACZ,WAAK,QAAQ,IAAI,GAAG,MAAM;AAC1B;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,aAAa;AAC7B,YAAM,eAAe;AACrB,WAAK,QAAQ,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,GAAG,MAAM;AAC5C;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,cAAc;AAC9B,YAAM,eAAe;AACrB,WAAK,QAAQ,KAAK,IAAI,QAAQ,GAAG,SAAS,CAAC,CAAC,GAAG,MAAM;AACrD;AAAA,IACF;AAEA,QAAI,MAAM,IAAI,WAAW,KAAK,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG;AACnD,YAAM,eAAe;AAAA,IACvB;AAAA,EACF;AAEA,QAAM,cAAc,CAAC,UAAkD;AACrE,QAAI,CAAC,cAAc,SAAU;AAC7B,UAAM,eAAe;AAErB,UAAM,OAAO,MAAM,cAAc,QAAQ,MAAM,EAAE,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,MAAM;AACnF,QAAI,CAAC,KAAM;AAEX,cAAU,MAAM,OAAO;AACvB,oBAAgB,KAAK,IAAI,KAAK,SAAS,GAAG,SAAS,CAAC,CAAC;AAErD,UAAM,aAAa,KAAK,IAAI,KAAK,QAAQ,SAAS,CAAC;AACnD,SAAK,QAAQ,UAAU,GAAG,MAAM;AAAA,EAClC;AAEA,SACE,oBAAC,SAAI,WAAW,CAAC,YAAY,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAC7D,gBAAM,IAAI,CAAC,OAAO,UAAU;AAC3B,UAAM,eAAe,iBAAiB,QAAQ,YAAY;AAC1D,UAAM,cAAc,QAAQ,WAAW;AACvC,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,KAAK,CAAC,OAAO;AACX,eAAK,QAAQ,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,MAAK;AAAA,QACL,WAAW;AAAA,QACX,WAAU;AAAA,QACV,SAAQ;AAAA,QACR,cAAc,UAAU,IAAI,kBAAkB;AAAA,QAC9C,WAAW,CAAC,YAAY,aAAa,cAAc,cAAc,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,QAC3F,UAAU,CAAC,MAAM,eAAe,OAAO,EAAE,OAAO,KAAK;AAAA,QACrD,WAAW,CAAC,MAAM,cAAc,OAAO,CAAC;AAAA,QACxC,SAAS;AAAA,QACT,cAAY,aAAa,QAAQ,CAAC;AAAA;AAAA,MAf7B;AAAA,IAgBP;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":["useCallback","useEffect","useRef","useRef","useCallback","useEffect"]}
@@ -0,0 +1,42 @@
1
+ .otp-root {
2
+ display: flex;
3
+ gap: 0.6rem;
4
+ align-items: center;
5
+ }
6
+
7
+ .otp-slot {
8
+ width: 2.75rem;
9
+ height: 3.2rem;
10
+ border: 1px solid #c9ced6;
11
+ border-radius: 0.65rem;
12
+ text-align: center;
13
+ font-size: 1.35rem;
14
+ font-weight: 600;
15
+ transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
16
+ }
17
+
18
+ .otp-slot:focus {
19
+ border-color: #0e7490;
20
+ box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.2);
21
+ outline: none;
22
+ }
23
+
24
+ .otp-slot.filled {
25
+ border-color: #1d4ed8;
26
+ }
27
+
28
+ .otp-slot.entered {
29
+ animation: otp-pop 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
30
+ }
31
+
32
+ @keyframes otp-pop {
33
+ 0% {
34
+ transform: scale(0.92);
35
+ }
36
+ 55% {
37
+ transform: scale(1.08);
38
+ }
39
+ 100% {
40
+ transform: scale(1);
41
+ }
42
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "otp-auto-fetch-input",
3
+ "version": "0.1.0",
4
+ "description": "Animated OTP input for React with WebOTP auto-fetch support",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./styles.css": "./dist/styles.css"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup && cp src/styles.css dist/styles.css",
23
+ "dev": "tsup --watch",
24
+ "clean": "rm -rf dist",
25
+ "verify": "npm run clean && npm run build",
26
+ "release": "standard-version",
27
+ "release:patch": "standard-version --release-as patch",
28
+ "release:minor": "standard-version --release-as minor",
29
+ "release:major": "standard-version --release-as major",
30
+ "prepublishOnly": "npm run build",
31
+ "publish:public": "npm publish --access public"
32
+ },
33
+ "keywords": [
34
+ "otp",
35
+ "react",
36
+ "webotp",
37
+ "one-time-password",
38
+ "2fa",
39
+ "input"
40
+ ],
41
+ "peerDependencies": {
42
+ "react": ">=18",
43
+ "react-dom": ">=18"
44
+ },
45
+ "devDependencies": {
46
+ "@types/react": "^18.3.18",
47
+ "@types/react-dom": "^18.3.5",
48
+ "standard-version": "^9.5.0",
49
+ "tsup": "^8.4.0",
50
+ "typescript": "^5.7.3"
51
+ }
52
+ }