UNPKG

@benny_gebeya/gebeya-whatsapp-otp

Version:

React WhatsApp OTP verification component library with Supabase integration for phone number authentication

1,049 lines (854 loc) β€’ 31 kB
# Gebeya WhatsApp OTP A React component library for WhatsApp-based OTP verification with Supabase integration. This package provides a complete solution for phone number authentication using WhatsApp OTP, including a provider pattern, modal interface, and comprehensive error handling. ## Features - πŸ” **WhatsApp OTP Verification**: Send and verify OTP codes via WhatsApp - βš›οΈ **React Context Provider**: Easy state management with React Context - 🎨 **Pre-built UI Components**: Ready-to-use modal and button components - πŸ“± **Country Selection**: Built-in country code selector with flags - πŸ”„ **Retry Logic**: Automatic attempt tracking and suspension handling - 🎯 **TypeScript Support**: Full TypeScript definitions included - 🎨 **Tailwind CSS**: Styled with Tailwind CSS classes - πŸ”§ **Customizable**: Flexible configuration and callback system ## Installation ```bash npm install @benny_gebeya/gebeya-whatsapp-otp ``` ### Peer Dependencies This package requires React 16.8.0 or higher: ```bash npm install react react-dom ``` ## Quick Start ### 1. Wrap your app with the provider ```tsx import React from "react"; import { WhatsAppOTPProvider } from "@benny_gebeya/gebeya-whatsapp-otp"; const config = { supabaseUrl: "https://your-project.supabase.co", supabaseKey: "your-anon-key", defaultCountry: "+251", // Optional: Default to Ethiopia }; const callbacks = { onSuccess: (phoneNumber: string) => { console.log("Phone verified:", phoneNumber); }, onError: (error: string) => { console.error("Verification error:", error); }, }; function App() { return ( <WhatsAppOTPProvider config={config} callbacks={callbacks}> <YourAppContent /> </WhatsAppOTPProvider> ); } ``` ### 2. Add the verification components ```tsx import React from "react"; import { VerifyWithWhatsAppButton, WhatsAppOTPModal, } from "@benny_gebeya/gebeya-whatsapp-otp"; function YourAppContent() { return ( <div> <h1>Welcome to our app</h1> <VerifyWithWhatsAppButton /> <WhatsAppOTPModal /> </div> ); } ``` ## API Reference ### WhatsAppOTPProvider The main provider component that manages OTP verification state. #### Props | Prop | Type | Required | Description | | ----------- | ---------------------- | -------- | ---------------------- | | `children` | `React.ReactNode` | βœ… | Child components | | `config` | `WhatsAppOTPConfig` | βœ… | Supabase configuration | | `callbacks` | `WhatsAppOTPCallbacks` | ❌ | Event callbacks | #### WhatsAppOTPConfig ```typescript interface WhatsAppOTPConfig { /** Supabase project URL */ supabaseUrl: string; /** Supabase anon/service role key */ supabaseKey: string; /** Default country code (e.g., "+251" for Ethiopia) */ defaultCountry?: string; } ``` #### WhatsAppOTPCallbacks ```typescript interface WhatsAppOTPCallbacks { /** Called when OTP verification is successful */ onSuccess?: (phoneNumber: string) => void; /** Called when an error occurs during the process */ onError?: (error: string) => void; /** Called when the verification step changes */ onStepChange?: (step: VerificationStep) => void; /** Called when a phone number gets suspended */ onSuspension?: (message: string) => void; } ``` ### VerifyWithWhatsAppButton A button component that opens the verification modal. #### Props | Prop | Type | Default | Description | | ----------- | ----------------------------------------------------------------------------- | ------------------------ | -------------------------- | | `children` | `React.ReactNode` | `"Verify with WhatsApp"` | Button text | | `className` | `string` | `""` | Additional CSS classes | | `variant` | `"default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| "link"` | `"default"` | Button style variant | | `size` | `"default" \| "sm" \| "lg" \| "icon"` | `"default"` | Button size | | `disabled` | `boolean` | `false` | Whether button is disabled | #### Example ```tsx <VerifyWithWhatsAppButton variant="outline" size="lg" className="my-custom-class" > Start Verification </VerifyWithWhatsAppButton> ``` ### WhatsAppOTPModal The modal component that handles the verification flow. #### Props | Prop | Type | Default | Description | | ----------- | ----------------- | ------------ | ---------------------- | | `countries` | `CountryOption[]` | Default list | Custom country options | #### CountryOption ```typescript interface CountryOption { /** Country calling code (e.g., "+251") */ code: string; /** Country name (e.g., "Ethiopia") */ country: string; /** Country flag emoji (e.g., "πŸ‡ͺπŸ‡Ή") */ flag: string; } ``` #### Example with custom countries ```tsx const customCountries = [ { code: "+1", country: "United States", flag: "πŸ‡ΊπŸ‡Έ" }, { code: "+44", country: "United Kingdom", flag: "πŸ‡¬πŸ‡§" }, { code: "+251", country: "Ethiopia", flag: "πŸ‡ͺπŸ‡Ή" }, ]; <WhatsAppOTPModal countries={customCountries} />; ``` ### useWhatsAppOTP Hook Access the OTP verification state and actions directly. ```typescript import { useWhatsAppOTPContext } from "@benny_gebeya/gebeya-whatsapp-otp"; function CustomComponent() { const { step, phoneNumber, isLoading, sendOTP, verifyOTP, openModal } = useWhatsAppOTPContext(); return ( <div> <p>Current step: {step}</p> <button onClick={openModal}>Open Verification</button> </div> ); } ``` #### Hook Return Value ```typescript interface WhatsAppOTPContextType { // State properties step: VerificationStep; countryCode: string; phoneNumber: string; otpCode: string; isLoading: boolean; attemptsLeft: number; maxAttemptsReached: boolean; isSuspended: boolean; suspensionMessage: string; isModalOpen: boolean; // Action functions setCountryCode: (code: string) => void; setPhoneNumber: (number: string) => void; setOtpCode: (code: string) => void; sendOTP: () => Promise<void>; verifyOTP: () => Promise<void>; resetFlow: () => void; openModal: () => void; closeModal: () => void; } ``` ## TypeScript Usage The package includes full TypeScript support. Import types as needed: ```typescript import { WhatsAppOTPConfig, WhatsAppOTPCallbacks, VerificationStep, CountryOption, WhatsAppOTPContextType, } from "@benny_gebeya/gebeya-whatsapp-otp"; // Example: Type-safe configuration const config: WhatsAppOTPConfig = { supabaseUrl: process.env.REACT_APP_SUPABASE_URL!, supabaseKey: process.env.REACT_APP_SUPABASE_ANON_KEY!, defaultCountry: "+251", }; // Example: Type-safe callbacks const callbacks: WhatsAppOTPCallbacks = { onSuccess: (phoneNumber: string) => { // Handle successful verification console.log(`Verified: ${phoneNumber}`); }, onError: (error: string) => { // Handle errors console.error("Verification failed:", error); }, onStepChange: (step: VerificationStep) => { // Track verification progress console.log(`Step changed to: ${step}`); }, }; ``` ## Advanced Examples ### Custom Error Handling ```tsx import React, { useState } from "react"; import { WhatsAppOTPProvider, VerifyWithWhatsAppButton, WhatsAppOTPModal, } from "@benny_gebeya/gebeya-whatsapp-otp"; function App() { const [error, setError] = useState<string>(""); const [isVerified, setIsVerified] = useState(false); const config = { supabaseUrl: "https://your-project.supabase.co", supabaseKey: "your-anon-key", }; const callbacks = { onSuccess: (phoneNumber: string) => { setIsVerified(true); setError(""); // Redirect or update UI }, onError: (error: string) => { setError(error); }, onSuspension: (message: string) => { setError(`Account suspended: ${message}`); }, }; return ( <WhatsAppOTPProvider config={config} callbacks={callbacks}> <div className="p-4"> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> {error} </div> )} {isVerified ? ( <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded"> Phone number verified successfully! </div> ) : ( <VerifyWithWhatsAppButton /> )} <WhatsAppOTPModal /> </div> </WhatsAppOTPProvider> ); } ``` ### Integration with Form Libraries ```tsx import React from "react"; import { useForm } from "react-hook-form"; import { WhatsAppOTPProvider, useWhatsAppOTPContext, } from "@benny_gebeya/gebeya-whatsapp-otp"; interface FormData { name: string; email: string; phoneVerified: boolean; } function RegistrationForm() { const { register, handleSubmit, setValue, watch } = useForm<FormData>(); const { step } = useWhatsAppOTPContext(); const phoneVerified = watch("phoneVerified"); const onSubmit = (data: FormData) => { if (!data.phoneVerified) { alert("Please verify your phone number first"); return; } // Submit form console.log("Form submitted:", data); }; const callbacks = { onSuccess: () => { setValue("phoneVerified", true); }, }; return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <input {...register("name", { required: true })} placeholder="Full Name" className="w-full p-2 border rounded" /> <input {...register("email", { required: true })} type="email" placeholder="Email" className="w-full p-2 border rounded" /> <div className="flex items-center gap-2"> <VerifyWithWhatsAppButton size="sm" /> {phoneVerified && <span className="text-green-600">βœ“ Verified</span>} </div> <button type="submit" disabled={!phoneVerified} className="w-full bg-blue-600 text-white p-2 rounded disabled:opacity-50" > Register </button> <WhatsAppOTPModal /> </form> ); } ``` ## Styling The components use Tailwind CSS classes and are designed to work with your existing Tailwind setup. The components include: - Responsive design - Dark mode support - Accessible focus states - Consistent spacing and typography ### Custom Styling You can override styles by passing custom `className` props: ```tsx <VerifyWithWhatsAppButton className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-full"> Custom Styled Button </VerifyWithWhatsAppButton> ``` ## Requirements - React 16.8.0 or higher - Supabase project with WhatsApp OTP function deployed - Tailwind CSS (for styling) - WhatsApp Business API access (Facebook/Meta) ## Supabase Setup This package requires a Supabase edge function and database tables to handle WhatsApp OTP functionality. Follow these steps to set up your Supabase project: ### 1. Create Database Tables Run these SQL commands in your Supabase SQL editor: #### Complete Database Schema Run this complete SQL script in your Supabase SQL editor to set up all required tables, indexes, and security policies: ```sql -- Set up database configuration SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); SET check_function_bodies = false; SET xmloption = content; SET client_min_messages = warning; SET row_security = off; -- Create extensions if they don't exist CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; -- Create utility function for updating timestamps CREATE OR REPLACE FUNCTION "public"."update_updated_at_column"() RETURNS "trigger" LANGUAGE "plpgsql" SET "search_path" TO '' AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$; -- Create OTP codes table CREATE TABLE IF NOT EXISTS "public"."otp_codes" ( "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, "phone_number" "text" NOT NULL, "code" "text" NOT NULL, "expires_at" timestamp with time zone NOT NULL, "used" boolean DEFAULT false NOT NULL, "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, "attempts_left" integer DEFAULT 3 NOT NULL, "suspended_until" timestamp with time zone, "suspension_reason" "text" ); -- Add table comment COMMENT ON COLUMN "public"."otp_codes"."expires_at" IS 'OTP codes should expire within 2-3 minutes for security'; -- Create primary key ALTER TABLE ONLY "public"."otp_codes" ADD CONSTRAINT "otp_codes_pkey" PRIMARY KEY ("id"); -- Create indexes for better performance CREATE INDEX "idx_otp_codes_expires_at" ON "public"."otp_codes" USING "btree" ("expires_at"); CREATE INDEX "idx_otp_codes_phone_number" ON "public"."otp_codes" USING "btree" ("phone_number"); CREATE INDEX "idx_otp_codes_phone_suspended" ON "public"."otp_codes" USING "btree" ("phone_number", "suspended_until"); -- Create Row Level Security policies CREATE POLICY "Anyone can insert OTP codes" ON "public"."otp_codes" FOR INSERT WITH CHECK (true); CREATE POLICY "Anyone can select OTP codes" ON "public"."otp_codes" FOR SELECT USING (true); CREATE POLICY "Anyone can update OTP codes" ON "public"."otp_codes" FOR UPDATE USING (true); -- Enable Row Level Security ALTER TABLE "public"."otp_codes" ENABLE ROW LEVEL SECURITY; -- Grant necessary permissions GRANT ALL ON TABLE "public"."otp_codes" TO "anon"; GRANT ALL ON TABLE "public"."otp_codes" TO "authenticated"; GRANT ALL ON TABLE "public"."otp_codes" TO "service_role"; GRANT ALL ON FUNCTION "public"."update_updated_at_column"() TO "anon"; GRANT ALL ON FUNCTION "public"."update_updated_at_column"() TO "authenticated"; GRANT ALL ON FUNCTION "public"."update_updated_at_column"() TO "service_role"; ``` ### 2. Deploy the Edge Function #### Step 1: Install Supabase CLI ```bash npm install -g supabase ``` #### Step 2: Login to Supabase ```bash supabase login ``` #### Step 3: Initialize Supabase in your project (if not already done) ```bash supabase init ``` #### Step 4: Create the edge function ```bash supabase functions new otp_whatsapp ``` #### Step 5: Replace the function code Replace the contents of `supabase/functions/otp_whatsapp/index.ts` with: ```typescript import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; serve(async (req) => { // Handle CORS preflight requests if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const supabase = createClient( Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_ANON_KEY") ?? "" ); // Create admin client for user creation const supabaseAdmin = createClient( Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "" ); const { action, phone_number, code } = await req.json(); if (action === "send_otp") { // Check if phone number is currently suspended const { data: suspensionCheck } = await supabase .from("otp_codes") .select("suspended_until, suspension_reason") .eq("phone_number", phone_number) .not("suspended_until", "is", null) .gt("suspended_until", new Date().toISOString()) .order("suspended_until", { ascending: false }) .limit(1); if (suspensionCheck && suspensionCheck.length > 0) { const suspension = suspensionCheck[0]; const remainingTime = Math.ceil( (new Date(suspension.suspended_until).getTime() - new Date().getTime()) / 1000 ); const minutes = Math.ceil(remainingTime / 60); return new Response( JSON.stringify({ error: `Phone number is temporarily suspended. Please try again in ${minutes} minute${ minutes !== 1 ? "s" : "" }.`, }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Generate 6-digit OTP const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const expiresAt = new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 2); // 2 minutes expiry console.log(`Generating OTP for phone ${phone_number}: ${otpCode}`); // Store OTP in database const { error: otpError } = await supabase.from("otp_codes").insert({ phone_number, code: otpCode, expires_at: expiresAt.toISOString(), suspended_until: null, suspension_reason: null, }); if (otpError) { console.error("Error storing OTP:", otpError); return new Response( JSON.stringify({ error: "Failed to generate OTP" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Send WhatsApp message using Facebook Graph API const facebookToken = Deno.env.get("FACEBOOK_ACCESS_TOKEN"); const phoneNumberId = Deno.env.get("WHATSAPP_PHONE_NUMBER_ID"); const whatsappPayload = { messaging_product: "whatsapp", to: phone_number, type: "template", template: { name: "otp_verification_code", language: { code: "en_US" }, components: [ { type: "BODY", parameters: [{ type: "text", text: otpCode }], }, { type: "BUTTON", sub_type: "URL", index: "0", parameters: [{ type: "text", text: otpCode }], }, ], }, }; const whatsappResponse = await fetch( `https://graph.facebook.com/v22.0/${phoneNumberId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${facebookToken}`, "Content-Type": "application/json", }, body: JSON.stringify(whatsappPayload), } ); const whatsappResult = await whatsappResponse.json(); if (!whatsappResponse.ok) { console.error("WhatsApp API error:", whatsappResult); return new Response( JSON.stringify({ error: "Failed to send WhatsApp message", details: whatsappResult, }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } return new Response( JSON.stringify({ success: true, message: "OTP sent successfully", message_id: whatsappResult.messages?.[0]?.id, }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } else if (action === "verify_otp") { // Check if phone number is currently suspended const { data: suspensionCheck } = await supabase .from("otp_codes") .select("suspended_until, suspension_reason") .eq("phone_number", phone_number) .not("suspended_until", "is", null) .gt("suspended_until", new Date().toISOString()) .order("suspended_until", { ascending: false }) .limit(1); if (suspensionCheck && suspensionCheck.length > 0) { const suspension = suspensionCheck[0]; const remainingTime = Math.ceil( (new Date(suspension.suspended_until).getTime() - new Date().getTime()) / 1000 ); const minutes = Math.ceil(remainingTime / 60); return new Response( JSON.stringify({ error: `Phone number is temporarily suspended. Please try again in ${minutes} minute${ minutes !== 1 ? "s" : "" }.`, }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Check if there's a valid OTP for this phone number const { data: otpRecord, error: fetchError } = await supabase .from("otp_codes") .select("*") .eq("phone_number", phone_number) .eq("used", false) .gt("expires_at", new Date().toISOString()) .order("created_at", { ascending: false }) .limit(1) .maybeSingle(); if (fetchError) { console.error("Error fetching OTP:", fetchError); return new Response(JSON.stringify({ error: "Verification failed" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } if (!otpRecord) { return new Response( JSON.stringify({ error: "Invalid or expired OTP code" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Check if the code matches if (otpRecord.code !== code) { const newAttemptsLeft = otpRecord.attempts_left - 1; await supabase .from("otp_codes") .update({ attempts_left: newAttemptsLeft }) .eq("id", otpRecord.id); if (newAttemptsLeft <= 0) { // Suspend the phone number for 3 minutes const suspendedUntil = new Date(); suspendedUntil.setMinutes(suspendedUntil.getMinutes() + 3); await supabase .from("otp_codes") .update({ used: true, suspended_until: suspendedUntil.toISOString(), suspension_reason: "max_attempts_reached", }) .eq("id", otpRecord.id); return new Response( JSON.stringify({ error: "Maximum attempts reached. Your phone number has been temporarily suspended for 3 minutes.", }), { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } return new Response( JSON.stringify({ error: `Invalid OTP code. ${newAttemptsLeft} attempts remaining.`, }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } // Mark OTP as used const { error: updateError } = await supabase .from("otp_codes") .update({ used: true }) .eq("id", otpRecord.id); if (updateError) { console.error("Error updating OTP:", updateError); return new Response(JSON.stringify({ error: "Verification failed" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } // Handle user authentication const { data: existingUsers } = await supabaseAdmin.auth.admin.listUsers(); const existingUser = existingUsers.users?.find( (u) => u.phone === phone_number ); let user; if (existingUser) { user = existingUser; } else { // Create new user with phone number const { data: newUserData, error: createError } = await supabaseAdmin.auth.admin.createUser({ phone: phone_number, phone_confirm: true, email_confirm: true, user_metadata: { phone: phone_number, provider: "phone", }, }); if (createError) { console.error("Error creating user:", createError); return new Response( JSON.stringify({ error: "Failed to create user account" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } user = newUserData.user; } // Send success template const facebookToken = Deno.env.get("FACEBOOK_ACCESS_TOKEN"); const phoneNumberId = Deno.env.get("WHATSAPP_PHONE_NUMBER_ID"); const successPayload = { messaging_product: "whatsapp", to: phone_number, type: "template", template: { name: "otp_success", language: { code: "en_US" }, }, }; await fetch( `https://graph.facebook.com/v22.0/${phoneNumberId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${facebookToken}`, "Content-Type": "application/json", }, body: JSON.stringify(successPayload), } ); return new Response( JSON.stringify({ success: true, message: "Phone number verified and user authenticated successfully", verified: true, user: user, }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, } ); } return new Response(JSON.stringify({ error: "Invalid action" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } catch (error) { console.error("Error in otp_whatsapp function:", error); return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } }); ``` #### Step 6: Set up environment variables ```bash # Set your environment variables supabase secrets set FACEBOOK_ACCESS_TOKEN=your_facebook_access_token ``` #### Step 7: Deploy the function ```bash supabase functions deploy otp_whatsapp ``` ### 3. WhatsApp Business API Setup 1. **Create a Meta Business Account** at https://business.facebook.com 2. **Set up WhatsApp Business API** through Meta Business 3. **Create message templates** in your WhatsApp Business Manager: - Template name: `otp_verification_code` - Template name: `otp_success` 4. **Get your access token** and phone number ID from the Meta Developer Console 5. **Configure webhooks** (optional) for delivery status ### 4. Update Your App Configuration Make sure your app configuration points to your Supabase project: ```tsx const config = { supabaseUrl: "https://your-project.supabase.co", supabaseKey: "your-anon-key", // This should be your anon/public key }; ``` ### 5. Test Your Setup You can test your edge function directly: ```bash curl -X POST 'https://your-project.supabase.co/functions/v1/otp_whatsapp' \ -H 'Authorization: Bearer YOUR_ANON_KEY' \ -H 'Content-Type: application/json' \ -d '{ "action": "send_otp", "phone_number": "+1234567890" }' ``` ## License MIT ## Support For issues and questions, please visit our [GitHub repository](https://github.com/gebeya/gebeya-whatsapp-otp). ## Development ### Package Validation This package includes a comprehensive validation script to ensure the build is correct and ready for publishing. The validation script tests: - Build file existence and formats - Package.json configuration - Import functionality in both JavaScript and TypeScript - Peer dependency resolution - Basic component functionality #### Running Validation ```bash # Run the full package validation npm run validate # Run just the build validation npm run validate-build ``` #### What Gets Validated 1. **Build Files**: Checks that all required build outputs exist - `dist/index.cjs` (CommonJS build) - `dist/index.esm.js` (ES modules build) - `dist/index.d.ts` (TypeScript declarations) - Component-specific declaration files 2. **Package Configuration**: Validates package.json settings - Entry points (main, module, types) - Peer dependencies configuration - File inclusion settings 3. **Import Testing**: Tests imports in different environments - CommonJS require() syntax - ES modules import syntax - TypeScript type checking and compilation 4. **Peer Dependencies**: Verifies React is properly externalized - Checks that React is not bundled in the output - Validates peer dependency resolution 5. **Component Functionality**: Basic validation of exported components - Ensures all exports are available - Validates component types and structure #### Validation Output The validation script provides detailed feedback: ``` πŸ” Comprehensive Package Validation for gebeya-whatsapp-otp 1. Checking build output files... βœ“ dist/index.cjs exists βœ“ dist/index.esm.js exists βœ“ dist/index.d.ts exists ... βœ… Package validation PASSED πŸ“¦ Package is ready for publishing ``` ### Building the Package ```bash # Build the package npm run build # Build and validate npm run build && npm run validate ``` The build process: 1. Compiles TypeScript to generate declaration files 2. Bundles with Rollup to create CommonJS and ES module outputs 3. Generates source maps for debugging 4. Excludes peer dependencies from the bundle