UNPKG

@turnkey/react-wallet-kit

Version:

The easiest and most powerful way to integrate Turnkey's Embedded Wallets into your React applications.

215 lines (211 loc) 7.68 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var Hook$1 = require('../../providers/modal/Hook.js'); var Hook = require('../../providers/client/Hook.js'); var Spinners = require('../design/Spinners.js'); var react$1 = require('@headlessui/react'); var Buttons = require('../design/Buttons.js'); var core = require('@turnkey/core'); var reactFontawesome = require('@fortawesome/react-fontawesome'); var freeSolidSvgIcons = require('@fortawesome/free-solid-svg-icons'); var clsx = require('clsx'); function OtpVerification(props) { const { contact, otpType, otpLength = 6, alphanumeric = true, formattedContact, sessionKey, onContinue = null // Default to null if not provided } = props; const { initOtp, completeOtp } = Hook.useTurnkey(); const { closeModal, isMobile } = Hook$1.useModal(); const [submitting, setSubmitting] = react.useState(false); const [resending, setResending] = react.useState(false); const [resent, setResent] = react.useState(false); const [otpId, setOtpId] = react.useState(props.otpId); const [otpEncryptionTargetBundle, setOtpEncryptionTargetBundle] = react.useState(props.otpEncryptionTargetBundle); const [error, setError] = react.useState(null); const [shaking, setShaking] = react.useState(false); const shakeInput = () => { setShaking(true); setTimeout(() => setShaking(false), 250); }; const handleContinue = async otpCode => { try { setSubmitting(true); if (onContinue) { await onContinue(otpCode); } else { await completeOtp({ otpId, otpCode, otpEncryptionTargetBundle, contact, otpType, ...(sessionKey && { sessionKey }) }); closeModal(); } } catch (error) { const niceError = error.code === core.TurnkeyErrorCodes.INVALID_OTP_CODE ? "Invalid OTP code" : "An error has occurred"; setError(niceError); shakeInput(); throw new Error(`Error completing OTP: ${error}`); } finally { setSubmitting(false); } }; const handleResend = async () => { setResending(true); try { const { otpId, otpEncryptionTargetBundle } = await initOtp({ otpType, contact }); setOtpId(otpId); setOtpEncryptionTargetBundle(otpEncryptionTargetBundle); setResent(true); } catch (error) { throw new Error(`Error resending OTP: ${error}`); } finally { setResending(false); } }; return jsxRuntime.jsxs("div", { className: clsx("flex items-center justify-center py-3", isMobile ? "w-full" : "min-w-96"), children: [jsxRuntime.jsxs("div", { className: `flex flex-col items-center justify-center gap-6 transition-all duration-300 ${submitting && "opacity-30 blur"}`, children: [jsxRuntime.jsx(reactFontawesome.FontAwesomeIcon, { size: "3x", icon: otpType === core.OtpType.Email ? freeSolidSvgIcons.faEnvelope : otpType === core.OtpType.Sms ? freeSolidSvgIcons.faPhone : freeSolidSvgIcons.faEnvelope }), jsxRuntime.jsxs("div", { className: "flex flex-col text-center", children: [jsxRuntime.jsx("span", { className: "text-lg font-medium", children: `Enter the ${otpLength}-digit code we sent to` }), jsxRuntime.jsx("span", { className: "text-base font-semibold", children: formattedContact ?? contact })] }), jsxRuntime.jsx("div", { className: `transition-all flex justify-center ${shaking ? "animate-shake" : ""}`, children: jsxRuntime.jsx(OtpInput, { otpLength: otpLength, onContinue: handleContinue, alphanumeric: alphanumeric }) }), error && jsxRuntime.jsx("div", { className: "text-red-400 text-center text-sm", children: error }), jsxRuntime.jsx(Buttons.BaseButton, { onClick: handleResend, disabled: resending || resent, className: `text-xs text-inherit font-semibold bg-transparent border-none ${resent && "opacity-30"}`, children: resending ? jsxRuntime.jsxs("span", { className: "flex items-center gap-2.5", children: [jsxRuntime.jsx(Spinners.Spinner, { className: "size-3" }), "Resending..."] }) : resent ? "Code sent!" : "Resend Code" })] }), submitting && jsxRuntime.jsx("div", { className: "absolute flex w-full h-full justify-center items-center", children: jsxRuntime.jsx(Spinners.Spinner, { strokeWidth: 1, className: "size-1/2" }) })] }); } function OtpInput(props) { const { otpLength, onContinue, alphanumeric = false } = props; const { isMobile } = Hook$1.useModal(); const [values, setValues] = react.useState(Array(otpLength).fill("")); const inputsRef = react.useRef([]); const handleChange = (index, value) => { if (!alphanumeric) value = value.replace(/[^0-9]/g, "");else value = value.replace(/[^a-zA-Z0-9]/g, ""); const newValues = [...values]; newValues[index] = value.slice(-1); // only last char setValues(newValues); if (value && index < otpLength - 1) { inputsRef.current[index + 1]?.focus(); } if (value && newValues.every(v => v)) { onContinue(newValues.join("")); } }; const handleKeyDown = (e, index) => { if (e.key === "Backspace" && !values[index] && index > 0) { inputsRef.current[index - 1]?.focus(); } if (e.key === "ArrowLeft" && index > 0) { inputsRef.current[index - 1]?.focus(); } if (e.key === "ArrowRight" && index < otpLength - 1) { inputsRef.current[index + 1]?.focus(); } if (e.key === "Enter") { onContinue(values.join("")); } }; const handlePaste = e => { e.preventDefault(); const paste = e.clipboardData.getData("text").trim(); const cleaned = alphanumeric ? paste.replace(/[^a-zA-Z0-9]/g, "") : paste.replace(/[^0-9]/g, ""); const sliced = cleaned.slice(0, otpLength).split(""); const newValues = [...values]; for (let i = 0; i < sliced.length; i++) { newValues[i] = sliced[i] ?? ""; if (inputsRef.current[i]) { inputsRef.current[i].value = sliced[i] ?? ""; } } setValues(newValues); // Focus the last filled box const lastIndex = Math.min(sliced.length, otpLength - 1); inputsRef.current[lastIndex]?.focus(); onContinue(newValues.join("")); }; return jsxRuntime.jsx("div", { className: clsx("flex items-center justify-center space-x-2", isMobile ? "w-[95%]" : "w-full"), children: Array.from({ length: otpLength }).map((_, i) => { return jsxRuntime.jsx(react$1.Input, { type: alphanumeric ? "text" : "text", inputMode: alphanumeric ? "text" : "numeric", maxLength: 1, value: values[i]?.toUpperCase(), autoComplete: "off", onChange: e => handleChange(i, e.target.value.toUpperCase()), onKeyDown: e => handleKeyDown(e, i), onPaste: handlePaste, ref: el => inputsRef.current[i] = el, className: clsx("text-center text-lg rounded-md border border-modal-background-dark/20 dark:border-modal-background-light/20 bg-button-light dark:bg-button-dark text-inherit focus:outline-primary-light focus:dark:outline-primary-dark focus:outline-[1px] focus:outline-offset-0 transition-all", isMobile ? "w-full h-10" : "h-12 w-12") }, i); }) }); } exports.OtpInput = OtpInput; exports.OtpVerification = OtpVerification; //# sourceMappingURL=OTP.js.map