@benny_gebeya/gebeya-whatsapp-otp
Version:
React WhatsApp OTP verification component library with Supabase integration for phone number authentication
163 lines (162 loc) • 15.3 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { useState } from "react";
import { Phone, CheckCircle, X, ChevronDown } from "lucide-react";
import { useWhatsAppOTPContext } from "./WhatsAppOTPProvider";
var Dialog = function (_a) {
var open = _a.open, onOpenChange = _a.onOpenChange, children = _a.children;
if (!open)
return null;
return (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [_jsx("div", { className: "fixed inset-0 bg-black/50", onClick: function () { return onOpenChange(false); } }), _jsx("div", { className: "relative z-50", children: children })] }));
};
var DialogContent = function (_a) {
var _b = _a.className, className = _b === void 0 ? "" : _b, children = _a.children;
return (_jsx("div", { className: "bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6 w-full max-w-md mx-4 ".concat(className), children: children }));
};
var DialogHeader = function (_a) {
var children = _a.children;
return (_jsx("div", { className: "mb-4", children: children }));
};
var DialogTitle = function (_a) {
var _b = _a.className, className = _b === void 0 ? "" : _b, children = _a.children;
return _jsx("h2", { className: "text-lg font-semibold ".concat(className), children: children });
};
var Button = React.forwardRef(function (_a, ref) {
var _b = _a.className, className = _b === void 0 ? "" : _b, _c = _a.variant, variant = _c === void 0 ? "default" : _c, _d = _a.size, size = _d === void 0 ? "default" : _d, props = __rest(_a, ["className", "variant", "size"]);
var baseClasses = "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
var variantClasses = {
default: "bg-primary text-primary-foreground hover:bg-primary/90 bg-gray-900 text-white hover:bg-gray-800",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 bg-red-600 text-white hover:bg-red-700",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground border-gray-300 hover:bg-gray-50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 bg-gray-100 text-gray-900 hover:bg-gray-200",
ghost: "hover:bg-accent hover:text-accent-foreground hover:bg-gray-100",
link: "text-primary underline-offset-4 hover:underline text-blue-600",
};
var sizeClasses = {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
};
var classes = "".concat(baseClasses, " ").concat(variantClasses[variant], " ").concat(sizeClasses[size], " ").concat(className);
return _jsx("button", __assign({ className: classes, ref: ref }, props));
});
Button.displayName = "Button";
var Input = React.forwardRef(function (_a, ref) {
var _b = _a.className, className = _b === void 0 ? "" : _b, props = __rest(_a, ["className"]);
var classes = "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 border-gray-300 focus:border-blue-500 focus:ring-blue-500 ".concat(className);
return _jsx("input", __assign({ className: classes, ref: ref }, props));
});
Input.displayName = "Input";
var Label = React.forwardRef(function (_a, ref) {
var _b = _a.className, className = _b === void 0 ? "" : _b, props = __rest(_a, ["className"]);
var classes = "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ".concat(className);
return _jsx("label", __assign({ className: classes, ref: ref }, props));
});
Label.displayName = "Label";
var Select = function (_a) {
var value = _a.value, onValueChange = _a.onValueChange, children = _a.children;
var _b = useState(false), isOpen = _b[0], setIsOpen = _b[1];
return (_jsx("div", { className: "relative", children: React.Children.map(children, function (child) {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
value: value,
onValueChange: onValueChange,
isOpen: isOpen,
setIsOpen: setIsOpen,
});
}
return child;
}) }));
};
var SelectTrigger = function (_a) {
var children = _a.children, isOpen = _a.isOpen, setIsOpen = _a.setIsOpen;
return (_jsxs("button", { type: "button", className: "flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", onClick: function () { return setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(!isOpen); }, children: [children, _jsx(ChevronDown, { className: "h-4 w-4 opacity-50" })] }));
};
var SelectValue = function (_a) {
var value = _a.value;
var selectedCountry = defaultCountries.find(function (country) { return country.code === value; });
return (_jsx("span", { className: "flex items-center gap-2", children: selectedCountry ? (_jsxs(_Fragment, { children: [_jsx("span", { children: selectedCountry.flag }), _jsxs("span", { children: [selectedCountry.country, " (", selectedCountry.code, ")"] })] })) : (_jsx("span", { className: "text-muted-foreground", children: "Select country..." })) }));
};
var SelectContent = function (_a) {
var children = _a.children, value = _a.value, onValueChange = _a.onValueChange, isOpen = _a.isOpen, setIsOpen = _a.setIsOpen;
if (!isOpen)
return null;
return (_jsx("div", { className: "absolute top-full left-0 right-0 z-50 mt-1 max-h-60 overflow-auto rounded-md border border-gray-300 bg-white shadow-lg", children: React.Children.map(children, function (child) {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
value: value,
onValueChange: onValueChange,
setIsOpen: setIsOpen,
});
}
return child;
}) }));
};
var SelectItem = function (_a) {
var itemValue = _a.value, children = _a.children, onValueChange = _a.onValueChange, setIsOpen = _a.setIsOpen;
return (_jsx("div", { className: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-gray-100 focus:bg-gray-100", onClick: function () {
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(itemValue);
setIsOpen === null || setIsOpen === void 0 ? void 0 : setIsOpen(false);
}, children: children }));
};
// Default countries list
var defaultCountries = [
{ code: "+251", country: "Ethiopia", flag: "🇪🇹" },
{ code: "+254", country: "Kenya", flag: "🇰🇪" },
{ code: "+256", country: "Uganda", flag: "🇺🇬" },
{ code: "+255", country: "Tanzania", flag: "🇹🇿" },
{ code: "+250", country: "Rwanda", flag: "🇷🇼" },
{ code: "+257", country: "Burundi", flag: "🇧🇮" },
{ code: "+252", country: "Somalia", flag: "🇸🇴" },
{ code: "+253", country: "Djibouti", flag: "🇩🇯" },
{ code: "+211", country: "South Sudan", flag: "🇸🇸" },
];
export var WhatsAppOTPModal = function (_a) {
var _b = _a.countries, countries = _b === void 0 ? defaultCountries : _b;
var _c = useWhatsAppOTPContext(), isModalOpen = _c.isModalOpen, closeModal = _c.closeModal, step = _c.step, countryCode = _c.countryCode, phoneNumber = _c.phoneNumber, otpCode = _c.otpCode, isLoading = _c.isLoading, attemptsLeft = _c.attemptsLeft, maxAttemptsReached = _c.maxAttemptsReached, isSuspended = _c.isSuspended, suspensionMessage = _c.suspensionMessage, setCountryCode = _c.setCountryCode, setPhoneNumber = _c.setPhoneNumber, setOtpCode = _c.setOtpCode, sendOTP = _c.sendOTP, verifyOTP = _c.verifyOTP, resetFlow = _c.resetFlow;
var handleBackToPhone = function () {
resetFlow();
};
return (_jsx(Dialog, { open: isModalOpen, onOpenChange: closeModal, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Phone, { className: "h-5 w-5 text-green-600" }), step === "phone" && "WhatsApp OTP", step === "verify" && "Enter Verification Code", step === "success" && "Phone Verified!"] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: closeModal, children: _jsx(X, { className: "h-4 w-4" }) })] }) }), _jsxs("div", { className: "space-y-4", children: [step === "phone" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Enter your phone number to receive a verification code via WhatsApp" }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "country", children: "Country" }), _jsxs(Select, { value: countryCode, onValueChange: setCountryCode, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: countries.map(function (country) { return (_jsx(SelectItem, { value: country.code, children: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { children: country.flag }), _jsxs("span", { children: [country.country, " (", country.code, ")"] })] }) }, country.code)); }) })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "phone", children: "Phone Number" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("div", { className: "flex items-center px-3 py-2 border rounded-md bg-muted text-muted-foreground", children: countryCode }), _jsx(Input, { id: "phone", type: "tel", placeholder: "985101186", value: phoneNumber, onChange: function (e) {
return setPhoneNumber(e.target.value.replace(/\D/g, ""));
}, className: "flex-1" })] })] }), _jsx(Button, { onClick: sendOTP, className: "w-full", disabled: !phoneNumber || isLoading || isSuspended, children: isLoading
? "Sending..."
: isSuspended
? "Phone Suspended"
: "Send OTP via WhatsApp" })] })), step === "verify" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Enter the 6-digit code sent to your WhatsApp" }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "otp", children: "Verification Code" }), _jsx(Input, { id: "otp", type: "text", placeholder: "123456", value: otpCode, onChange: function (e) {
return setOtpCode(e.target.value.replace(/\D/g, "").slice(0, 6));
}, maxLength: 6, className: "text-center text-lg tracking-widest", disabled: maxAttemptsReached || isSuspended })] }), _jsx("div", { className: "text-center", children: isSuspended ? (_jsxs("div", { className: "p-3 bg-destructive/10 border border-destructive/20 rounded-md", children: [_jsx("p", { className: "text-destructive font-medium mb-1", children: "Phone Number Suspended" }), _jsx("p", { className: "text-sm text-muted-foreground", children: suspensionMessage })] })) : maxAttemptsReached ? (_jsxs("div", { className: "p-3 bg-destructive/10 border border-destructive/20 rounded-md", children: [_jsx("p", { className: "text-destructive font-medium", children: "Maximum attempts reached" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Please request a new OTP code" })] })) : (_jsx("div", { className: "p-2 rounded-md ".concat(attemptsLeft === 1
? "bg-destructive/10 border border-destructive/20"
: attemptsLeft === 2
? "bg-yellow-50 border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800"
: "bg-muted/50"), children: _jsxs("p", { className: "text-sm font-medium ".concat(attemptsLeft === 1
? "text-destructive"
: attemptsLeft === 2
? "text-yellow-700 dark:text-yellow-300"
: "text-muted-foreground"), children: [attemptsLeft, " ", attemptsLeft === 1 ? "attempt" : "attempts", " remaining"] }) })) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: handleBackToPhone, className: "flex-1", children: isSuspended || maxAttemptsReached ? "Get New Code" : "Back" }), _jsx(Button, { onClick: verifyOTP, className: "flex-1", disabled: otpCode.length !== 6 ||
isLoading ||
maxAttemptsReached ||
isSuspended, children: isLoading ? "Verifying..." : "Verify Code" })] })] })), step === "success" && (_jsxs("div", { className: "text-center space-y-4", children: [_jsx(CheckCircle, { className: "h-16 w-16 text-green-600 mx-auto" }), _jsx("p", { className: "text-lg font-semibold", children: "Phone number verified!" }), _jsxs("p", { className: "text-muted-foreground", children: [countryCode, phoneNumber, " has been successfully verified"] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: closeModal, className: "flex-1", children: "Close" }), _jsx(Button, { onClick: handleBackToPhone, className: "flex-1", children: "Verify Another" })] })] }))] })] }) }));
};