gebeya-whatsapp-otp
Version:
React WhatsApp OTP verification component with Supabase integration
181 lines (180 loc) • 7.12 kB
JavaScript
import { useState, useCallback } from "react";
export const useWhatsAppOTP = (config, callbacks) => {
const [step, setStep] = useState("phone");
const [countryCode, setCountryCode] = useState(config.defaultCountry || "+251");
const [phoneNumber, setPhoneNumber] = useState("");
const [otpCode, setOtpCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [attemptsLeft, setAttemptsLeft] = useState(3);
const [maxAttemptsReached, setMaxAttemptsReached] = useState(false);
const [isSuspended, setIsSuspended] = useState(false);
const [suspensionMessage, setSuspensionMessage] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const sendOTP = useCallback(async () => {
if (!phoneNumber)
return;
setIsLoading(true);
const fullNumber = countryCode + phoneNumber;
try {
const response = await fetch(`${config.supabaseUrl}/functions/v1/otp_whatsapp`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.supabaseKey}`,
"Content-Type": "application/json",
apikey: config.supabaseKey,
},
body: JSON.stringify({
action: "send_otp",
phone_number: fullNumber,
}),
});
const data = await response.json();
if (response.ok && data.success) {
setStep("verify");
setAttemptsLeft(3);
setMaxAttemptsReached(false);
setIsSuspended(false);
setSuspensionMessage("");
callbacks?.onStepChange?.("verify");
}
else {
throw new Error(data.error || "Failed to send OTP");
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to send OTP";
if (errorMessage.includes("temporarily suspended")) {
setIsSuspended(true);
setSuspensionMessage(errorMessage);
callbacks?.onSuspension?.(errorMessage);
}
callbacks?.onError?.(errorMessage);
}
finally {
setIsLoading(false);
}
}, [phoneNumber, countryCode, config, callbacks]);
const verifyOTP = useCallback(async () => {
if (!otpCode)
return;
setIsLoading(true);
const fullNumber = countryCode + phoneNumber;
try {
const response = await fetch(`${config.supabaseUrl}/functions/v1/otp_whatsapp`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.supabaseKey}`,
"Content-Type": "application/json",
apikey: config.supabaseKey,
},
body: JSON.stringify({
action: "verify_otp",
phone_number: fullNumber,
code: otpCode,
redirect_url: config.redirectUrl || "/dashboard",
}),
});
const data = await response.json();
if (response.ok && data?.success) {
setStep("success");
callbacks?.onStepChange?.("success");
callbacks?.onSuccess?.(fullNumber);
// Handle authentication and redirect
if (data.auth_url) {
console.log("Using auth URL for authentication:", data.auth_url);
// Use the auth URL for automatic authentication - this will establish the Supabase session
window.location.href = data.auth_url;
}
else if (data.redirect_url) {
console.log("Redirecting to:", data.redirect_url);
// Fallback to redirect URL
setTimeout(() => {
window.location.href = data.redirect_url;
}, 2000); // Give user time to see success message
}
// Store session data for reference
if (data.access_token) {
localStorage.setItem("whatsapp_otp_session", JSON.stringify({
access_token: data.access_token,
refresh_token: data.refresh_token,
user: data.user,
verified_at: new Date().toISOString(),
}));
}
}
else {
const errorMessage = data?.error || "Invalid or expired OTP code";
if (errorMessage.includes("temporarily suspended")) {
setIsSuspended(true);
setSuspensionMessage(errorMessage);
setMaxAttemptsReached(true);
setAttemptsLeft(0);
callbacks?.onSuspension?.(errorMessage);
}
else if (errorMessage.includes("Maximum attempts reached")) {
setMaxAttemptsReached(true);
setAttemptsLeft(0);
setIsSuspended(true);
setSuspensionMessage(errorMessage);
callbacks?.onSuspension?.(errorMessage);
}
else if (errorMessage.includes("attempts remaining")) {
const match = errorMessage.match(/(\d+)\s+attempts?\s+remaining/);
if (match) {
const remaining = parseInt(match[1]);
setAttemptsLeft(remaining);
}
}
callbacks?.onError?.(errorMessage);
setOtpCode("");
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Something went wrong";
callbacks?.onError?.(errorMessage);
setOtpCode("");
}
finally {
setIsLoading(false);
}
}, [otpCode, phoneNumber, countryCode, config, callbacks]);
const resetFlow = useCallback(() => {
setStep("phone");
setPhoneNumber("");
setOtpCode("");
setAttemptsLeft(3);
setMaxAttemptsReached(false);
setIsSuspended(false);
setSuspensionMessage("");
callbacks?.onStepChange?.("phone");
}, [callbacks]);
const openModal = useCallback(() => {
setIsModalOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsModalOpen(false);
resetFlow();
}, [resetFlow]);
return {
// State
step,
countryCode,
phoneNumber,
otpCode,
isLoading,
attemptsLeft,
maxAttemptsReached,
isSuspended,
suspensionMessage,
isModalOpen,
// Actions
setCountryCode,
setPhoneNumber,
setOtpCode,
sendOTP,
verifyOTP,
resetFlow,
openModal,
closeModal,
};
};