npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@benny_gebeya/gebeya-whatsapp-otp

v1.0.1

Published

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

Readme

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

npm install @benny_gebeya/gebeya-whatsapp-otp

Peer Dependencies

This package requires React 16.8.0 or higher:

npm install react react-dom

Quick Start

1. Wrap your app with the provider

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

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

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

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

<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

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

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.

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

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:

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

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

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:

<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:

-- 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

npm install -g supabase

Step 2: Login to Supabase

supabase login

Step 3: Initialize Supabase in your project (if not already done)

supabase init

Step 4: Create the edge function

supabase functions new otp_whatsapp

Step 5: Replace the function code

Replace the contents of supabase/functions/otp_whatsapp/index.ts with:

import { serve } from "https://deno.land/[email protected]/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

# Set your environment variables
supabase secrets set FACEBOOK_ACCESS_TOKEN=your_facebook_access_token

Step 7: Deploy the function

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:

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:

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.

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

# 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

# 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