@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
JavaScript
;
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