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 +104 -0
- package/dist/index.cjs +225 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +42 -0
- package/package.json +52 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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":"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","names":["useCallback","useEffect","useRef","useRef","useCallback","useEffect"]}
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|