{"_id":"@benny_gebeya/gebeya-whatsapp-otp","_rev":"2-c4c14e50580347675d0354044b5bc893","name":"@benny_gebeya/gebeya-whatsapp-otp","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@benny_gebeya/gebeya-whatsapp-otp","version":"1.0.0","keywords":["react","whatsapp","otp","verification","supabase","authentication","phone-verification","sms","gebeya","typescript","react-components"],"author":{"name":"Gebeya"},"license":"MIT","_id":"@benny_gebeya/gebeya-whatsapp-otp@1.0.0","maintainers":[{"name":"benny_gebeya","email":"biniyam.ayele@gebeya.com"}],"homepage":"https://github.com/gebeya/gebeya-whatsapp-otp#readme","bugs":{"url":"https://github.com/gebeya/gebeya-whatsapp-otp/issues"},"dist":{"shasum":"c8178452bce1fec48c14fa9ef863408b1501e2fc","tarball":"https://registry.npmjs.org/@benny_gebeya/gebeya-whatsapp-otp/-/gebeya-whatsapp-otp-1.0.0.tgz","fileCount":27,"integrity":"sha512-n2E6mS5cmUEpTb56yg3fE9VDUWDqTwrclg3e6Kowdhy2e+rgppLXTu9mh3y8AcsojFz+H61rQj2i/bM5kzI8hQ==","signatures":[{"sig":"MEQCIBD3GRTCEX1bpIVKroasAphJHwAgwbh1ayDB7xVaUlKYAiB7d116XVVVE33E3gJhQX7MyYkeMWbpTGbJJ52Ia2X94g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":273249},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.esm.js","scripts":{"dev":"rollup -c -w","lint":"eslint src --ext .ts,.tsx","test":"jest","build":"tsc && rollup -c","prepare":"npm run build","validate":"node validate-package.js","validate-build":"node test-build-validation.js"},"_npmUser":{"name":"benny_gebeya","email":"biniyam.ayele@gebeya.com"},"repository":{"url":"git+https://github.com/gebeya/gebeya-whatsapp-otp.git","type":"git"},"_npmVersion":"10.8.3","description":"React WhatsApp OTP verification component library with Supabase integration for phone number authentication","directories":{},"_nodeVersion":"20.13.1","dependencies":{"clsx":"^2.1.1","lucide-react":"^0.462.0","tailwind-merge":"^2.5.2","@radix-ui/react-label":"^2.1.0","@supabase/supabase-js":"^2.39.0","@radix-ui/react-dialog":"^1.1.2","@radix-ui/react-select":"^2.1.1","class-variance-authority":"^0.7.1"},"_hasShrinkwrap":false,"devDependencies":{"rollup":"^4.0.0","typescript":"^5.0.0","@types/react":"^18.3.1","@types/react-dom":"^18.3.1","rollup-plugin-postcss":"^4.0.2","@rollup/plugin-typescript":"^11.0.0","rollup-plugin-peer-deps-external":"^2.2.4"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0"},"_npmOperationalInternal":{"tmp":"tmp/gebeya-whatsapp-otp_1.0.0_1753955212477_0.03180415652615465","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@benny_gebeya/gebeya-whatsapp-otp","version":"1.0.1","description":"React WhatsApp OTP verification component library with Supabase integration for phone number authentication","type":"module","main":"dist/index.cjs","module":"dist/index.esm.js","types":"dist/index.d.ts","scripts":{"build":"tsc && rollup -c","dev":"rollup -c -w","test":"jest","lint":"eslint src --ext .ts,.tsx","prepare":"npm run build","validate":"node validate-package.js","validate-build":"node test-build-validation.js"},"keywords":["react","whatsapp","otp","verification","supabase","authentication","phone-verification","sms","gebeya","typescript","react-components"],"author":{"name":"Gebeya"},"license":"MIT","peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0"},"dependencies":{"@radix-ui/react-dialog":"^1.1.2","@radix-ui/react-label":"^2.1.0","@radix-ui/react-select":"^2.1.1","@supabase/supabase-js":"^2.39.0","class-variance-authority":"^0.7.1","clsx":"^2.1.1","lucide-react":"^0.462.0","tailwind-merge":"^2.5.2"},"devDependencies":{"@types/react":"^18.3.1","@types/react-dom":"^18.3.1","typescript":"^5.0.0","rollup":"^4.0.0","@rollup/plugin-typescript":"^11.0.0","rollup-plugin-peer-deps-external":"^2.2.4","rollup-plugin-postcss":"^4.0.2"},"repository":{"type":"git","url":"git+https://github.com/gebeya/gebeya-whatsapp-otp.git"},"bugs":{"url":"https://github.com/gebeya/gebeya-whatsapp-otp/issues"},"homepage":"https://github.com/gebeya/gebeya-whatsapp-otp#readme","_id":"@benny_gebeya/gebeya-whatsapp-otp@1.0.1","gitHead":"e91182295ad27f99b7aa406475b21fc190ec3d5e","_nodeVersion":"20.13.1","_npmVersion":"10.8.3","dist":{"integrity":"sha512-SicfDJGeS1VgN355VyD1SbSZtiJPuXobz9ui86V/xt75t5nzNG7b7/8bqRGRudpgNApfZIpoxahPFA73T9XC8Q==","shasum":"85b560a5fe91550d618a59271df80b47c7af10f2","tarball":"https://registry.npmjs.org/@benny_gebeya/gebeya-whatsapp-otp/-/gebeya-whatsapp-otp-1.0.1.tgz","fileCount":27,"unpackedSize":290203,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAOZPfKuCrGh/EN3eEkXG1oGG2u7MIM/13Tzh8bJymouAiEA51sCwMamJNwnL3Oa/WHEe9dsAjwGccNP91ZdwzGwz8E="}]},"_npmUser":{"name":"benny_gebeya","email":"biniyam.ayele@gebeya.com"},"directories":{},"maintainers":[{"name":"benny_gebeya","email":"biniyam.ayele@gebeya.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gebeya-whatsapp-otp_1.0.1_1753961833167_0.8406322887356419"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-31T09:46:52.385Z","modified":"2025-07-31T11:37:13.534Z","1.0.0":"2025-07-31T09:46:52.679Z","1.0.1":"2025-07-31T11:37:13.370Z"},"bugs":{"url":"https://github.com/gebeya/gebeya-whatsapp-otp/issues"},"author":{"name":"Gebeya"},"license":"MIT","homepage":"https://github.com/gebeya/gebeya-whatsapp-otp#readme","keywords":["react","whatsapp","otp","verification","supabase","authentication","phone-verification","sms","gebeya","typescript","react-components"],"repository":{"type":"git","url":"git+https://github.com/gebeya/gebeya-whatsapp-otp.git"},"description":"React WhatsApp OTP verification component library with Supabase integration for phone number authentication","maintainers":[{"name":"benny_gebeya","email":"biniyam.ayele@gebeya.com"}],"readme":"# Gebeya WhatsApp OTP\r\n\r\nA 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.\r\n\r\n## Features\r\n\r\n- 🔐 **WhatsApp OTP Verification**: Send and verify OTP codes via WhatsApp\r\n- ⚛️ **React Context Provider**: Easy state management with React Context\r\n- 🎨 **Pre-built UI Components**: Ready-to-use modal and button components\r\n- 📱 **Country Selection**: Built-in country code selector with flags\r\n- 🔄 **Retry Logic**: Automatic attempt tracking and suspension handling\r\n- 🎯 **TypeScript Support**: Full TypeScript definitions included\r\n- 🎨 **Tailwind CSS**: Styled with Tailwind CSS classes\r\n- 🔧 **Customizable**: Flexible configuration and callback system\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @benny_gebeya/gebeya-whatsapp-otp\r\n```\r\n\r\n### Peer Dependencies\r\n\r\nThis package requires React 16.8.0 or higher:\r\n\r\n```bash\r\nnpm install react react-dom\r\n```\r\n\r\n## Quick Start\r\n\r\n### 1. Wrap your app with the provider\r\n\r\n```tsx\r\nimport React from \"react\";\r\nimport { WhatsAppOTPProvider } from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\nconst config = {\r\n  supabaseUrl: \"https://your-project.supabase.co\",\r\n  supabaseKey: \"your-anon-key\",\r\n  defaultCountry: \"+251\", // Optional: Default to Ethiopia\r\n};\r\n\r\nconst callbacks = {\r\n  onSuccess: (phoneNumber: string) => {\r\n    console.log(\"Phone verified:\", phoneNumber);\r\n  },\r\n  onError: (error: string) => {\r\n    console.error(\"Verification error:\", error);\r\n  },\r\n};\r\n\r\nfunction App() {\r\n  return (\r\n    <WhatsAppOTPProvider config={config} callbacks={callbacks}>\r\n      <YourAppContent />\r\n    </WhatsAppOTPProvider>\r\n  );\r\n}\r\n```\r\n\r\n### 2. Add the verification components\r\n\r\n```tsx\r\nimport React from \"react\";\r\nimport {\r\n  VerifyWithWhatsAppButton,\r\n  WhatsAppOTPModal,\r\n} from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\nfunction YourAppContent() {\r\n  return (\r\n    <div>\r\n      <h1>Welcome to our app</h1>\r\n      <VerifyWithWhatsAppButton />\r\n      <WhatsAppOTPModal />\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n## API Reference\r\n\r\n### WhatsAppOTPProvider\r\n\r\nThe main provider component that manages OTP verification state.\r\n\r\n#### Props\r\n\r\n| Prop        | Type                   | Required | Description            |\r\n| ----------- | ---------------------- | -------- | ---------------------- |\r\n| `children`  | `React.ReactNode`      | ✅       | Child components       |\r\n| `config`    | `WhatsAppOTPConfig`    | ✅       | Supabase configuration |\r\n| `callbacks` | `WhatsAppOTPCallbacks` | ❌       | Event callbacks        |\r\n\r\n#### WhatsAppOTPConfig\r\n\r\n```typescript\r\ninterface WhatsAppOTPConfig {\r\n  /** Supabase project URL */\r\n  supabaseUrl: string;\r\n  /** Supabase anon/service role key */\r\n  supabaseKey: string;\r\n  /** Default country code (e.g., \"+251\" for Ethiopia) */\r\n  defaultCountry?: string;\r\n}\r\n```\r\n\r\n#### WhatsAppOTPCallbacks\r\n\r\n```typescript\r\ninterface WhatsAppOTPCallbacks {\r\n  /** Called when OTP verification is successful */\r\n  onSuccess?: (phoneNumber: string) => void;\r\n  /** Called when an error occurs during the process */\r\n  onError?: (error: string) => void;\r\n  /** Called when the verification step changes */\r\n  onStepChange?: (step: VerificationStep) => void;\r\n  /** Called when a phone number gets suspended */\r\n  onSuspension?: (message: string) => void;\r\n}\r\n```\r\n\r\n### VerifyWithWhatsAppButton\r\n\r\nA button component that opens the verification modal.\r\n\r\n#### Props\r\n\r\n| Prop        | Type                                                                          | Default                  | Description                |\r\n| ----------- | ----------------------------------------------------------------------------- | ------------------------ | -------------------------- |\r\n| `children`  | `React.ReactNode`                                                             | `\"Verify with WhatsApp\"` | Button text                |\r\n| `className` | `string`                                                                      | `\"\"`                     | Additional CSS classes     |\r\n| `variant`   | `\"default\" \\| \"destructive\" \\| \"outline\" \\| \"secondary\" \\| \"ghost\" \\| \"link\"` | `\"default\"`              | Button style variant       |\r\n| `size`      | `\"default\" \\| \"sm\" \\| \"lg\" \\| \"icon\"`                                         | `\"default\"`              | Button size                |\r\n| `disabled`  | `boolean`                                                                     | `false`                  | Whether button is disabled |\r\n\r\n#### Example\r\n\r\n```tsx\r\n<VerifyWithWhatsAppButton\r\n  variant=\"outline\"\r\n  size=\"lg\"\r\n  className=\"my-custom-class\"\r\n>\r\n  Start Verification\r\n</VerifyWithWhatsAppButton>\r\n```\r\n\r\n### WhatsAppOTPModal\r\n\r\nThe modal component that handles the verification flow.\r\n\r\n#### Props\r\n\r\n| Prop        | Type              | Default      | Description            |\r\n| ----------- | ----------------- | ------------ | ---------------------- |\r\n| `countries` | `CountryOption[]` | Default list | Custom country options |\r\n\r\n#### CountryOption\r\n\r\n```typescript\r\ninterface CountryOption {\r\n  /** Country calling code (e.g., \"+251\") */\r\n  code: string;\r\n  /** Country name (e.g., \"Ethiopia\") */\r\n  country: string;\r\n  /** Country flag emoji (e.g., \"🇪🇹\") */\r\n  flag: string;\r\n}\r\n```\r\n\r\n#### Example with custom countries\r\n\r\n```tsx\r\nconst customCountries = [\r\n  { code: \"+1\", country: \"United States\", flag: \"🇺🇸\" },\r\n  { code: \"+44\", country: \"United Kingdom\", flag: \"🇬🇧\" },\r\n  { code: \"+251\", country: \"Ethiopia\", flag: \"🇪🇹\" },\r\n];\r\n\r\n<WhatsAppOTPModal countries={customCountries} />;\r\n```\r\n\r\n### useWhatsAppOTP Hook\r\n\r\nAccess the OTP verification state and actions directly.\r\n\r\n```typescript\r\nimport { useWhatsAppOTPContext } from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\nfunction CustomComponent() {\r\n  const { step, phoneNumber, isLoading, sendOTP, verifyOTP, openModal } =\r\n    useWhatsAppOTPContext();\r\n\r\n  return (\r\n    <div>\r\n      <p>Current step: {step}</p>\r\n      <button onClick={openModal}>Open Verification</button>\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n#### Hook Return Value\r\n\r\n```typescript\r\ninterface WhatsAppOTPContextType {\r\n  // State properties\r\n  step: VerificationStep;\r\n  countryCode: string;\r\n  phoneNumber: string;\r\n  otpCode: string;\r\n  isLoading: boolean;\r\n  attemptsLeft: number;\r\n  maxAttemptsReached: boolean;\r\n  isSuspended: boolean;\r\n  suspensionMessage: string;\r\n  isModalOpen: boolean;\r\n\r\n  // Action functions\r\n  setCountryCode: (code: string) => void;\r\n  setPhoneNumber: (number: string) => void;\r\n  setOtpCode: (code: string) => void;\r\n  sendOTP: () => Promise<void>;\r\n  verifyOTP: () => Promise<void>;\r\n  resetFlow: () => void;\r\n  openModal: () => void;\r\n  closeModal: () => void;\r\n}\r\n```\r\n\r\n## TypeScript Usage\r\n\r\nThe package includes full TypeScript support. Import types as needed:\r\n\r\n```typescript\r\nimport {\r\n  WhatsAppOTPConfig,\r\n  WhatsAppOTPCallbacks,\r\n  VerificationStep,\r\n  CountryOption,\r\n  WhatsAppOTPContextType,\r\n} from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\n// Example: Type-safe configuration\r\nconst config: WhatsAppOTPConfig = {\r\n  supabaseUrl: process.env.REACT_APP_SUPABASE_URL!,\r\n  supabaseKey: process.env.REACT_APP_SUPABASE_ANON_KEY!,\r\n  defaultCountry: \"+251\",\r\n};\r\n\r\n// Example: Type-safe callbacks\r\nconst callbacks: WhatsAppOTPCallbacks = {\r\n  onSuccess: (phoneNumber: string) => {\r\n    // Handle successful verification\r\n    console.log(`Verified: ${phoneNumber}`);\r\n  },\r\n  onError: (error: string) => {\r\n    // Handle errors\r\n    console.error(\"Verification failed:\", error);\r\n  },\r\n  onStepChange: (step: VerificationStep) => {\r\n    // Track verification progress\r\n    console.log(`Step changed to: ${step}`);\r\n  },\r\n};\r\n```\r\n\r\n## Advanced Examples\r\n\r\n### Custom Error Handling\r\n\r\n```tsx\r\nimport React, { useState } from \"react\";\r\nimport {\r\n  WhatsAppOTPProvider,\r\n  VerifyWithWhatsAppButton,\r\n  WhatsAppOTPModal,\r\n} from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\nfunction App() {\r\n  const [error, setError] = useState<string>(\"\");\r\n  const [isVerified, setIsVerified] = useState(false);\r\n\r\n  const config = {\r\n    supabaseUrl: \"https://your-project.supabase.co\",\r\n    supabaseKey: \"your-anon-key\",\r\n  };\r\n\r\n  const callbacks = {\r\n    onSuccess: (phoneNumber: string) => {\r\n      setIsVerified(true);\r\n      setError(\"\");\r\n      // Redirect or update UI\r\n    },\r\n    onError: (error: string) => {\r\n      setError(error);\r\n    },\r\n    onSuspension: (message: string) => {\r\n      setError(`Account suspended: ${message}`);\r\n    },\r\n  };\r\n\r\n  return (\r\n    <WhatsAppOTPProvider config={config} callbacks={callbacks}>\r\n      <div className=\"p-4\">\r\n        {error && (\r\n          <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4\">\r\n            {error}\r\n          </div>\r\n        )}\r\n\r\n        {isVerified ? (\r\n          <div className=\"bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded\">\r\n            Phone number verified successfully!\r\n          </div>\r\n        ) : (\r\n          <VerifyWithWhatsAppButton />\r\n        )}\r\n\r\n        <WhatsAppOTPModal />\r\n      </div>\r\n    </WhatsAppOTPProvider>\r\n  );\r\n}\r\n```\r\n\r\n### Integration with Form Libraries\r\n\r\n```tsx\r\nimport React from \"react\";\r\nimport { useForm } from \"react-hook-form\";\r\nimport {\r\n  WhatsAppOTPProvider,\r\n  useWhatsAppOTPContext,\r\n} from \"@benny_gebeya/gebeya-whatsapp-otp\";\r\n\r\ninterface FormData {\r\n  name: string;\r\n  email: string;\r\n  phoneVerified: boolean;\r\n}\r\n\r\nfunction RegistrationForm() {\r\n  const { register, handleSubmit, setValue, watch } = useForm<FormData>();\r\n  const { step } = useWhatsAppOTPContext();\r\n\r\n  const phoneVerified = watch(\"phoneVerified\");\r\n\r\n  const onSubmit = (data: FormData) => {\r\n    if (!data.phoneVerified) {\r\n      alert(\"Please verify your phone number first\");\r\n      return;\r\n    }\r\n    // Submit form\r\n    console.log(\"Form submitted:\", data);\r\n  };\r\n\r\n  const callbacks = {\r\n    onSuccess: () => {\r\n      setValue(\"phoneVerified\", true);\r\n    },\r\n  };\r\n\r\n  return (\r\n    <form onSubmit={handleSubmit(onSubmit)} className=\"space-y-4\">\r\n      <input\r\n        {...register(\"name\", { required: true })}\r\n        placeholder=\"Full Name\"\r\n        className=\"w-full p-2 border rounded\"\r\n      />\r\n\r\n      <input\r\n        {...register(\"email\", { required: true })}\r\n        type=\"email\"\r\n        placeholder=\"Email\"\r\n        className=\"w-full p-2 border rounded\"\r\n      />\r\n\r\n      <div className=\"flex items-center gap-2\">\r\n        <VerifyWithWhatsAppButton size=\"sm\" />\r\n        {phoneVerified && <span className=\"text-green-600\">✓ Verified</span>}\r\n      </div>\r\n\r\n      <button\r\n        type=\"submit\"\r\n        disabled={!phoneVerified}\r\n        className=\"w-full bg-blue-600 text-white p-2 rounded disabled:opacity-50\"\r\n      >\r\n        Register\r\n      </button>\r\n\r\n      <WhatsAppOTPModal />\r\n    </form>\r\n  );\r\n}\r\n```\r\n\r\n## Styling\r\n\r\nThe components use Tailwind CSS classes and are designed to work with your existing Tailwind setup. The components include:\r\n\r\n- Responsive design\r\n- Dark mode support\r\n- Accessible focus states\r\n- Consistent spacing and typography\r\n\r\n### Custom Styling\r\n\r\nYou can override styles by passing custom `className` props:\r\n\r\n```tsx\r\n<VerifyWithWhatsAppButton className=\"bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-full\">\r\n  Custom Styled Button\r\n</VerifyWithWhatsAppButton>\r\n```\r\n\r\n## Requirements\r\n\r\n- React 16.8.0 or higher\r\n- Supabase project with WhatsApp OTP function deployed\r\n- Tailwind CSS (for styling)\r\n- WhatsApp Business API access (Facebook/Meta)\r\n\r\n## Supabase Setup\r\n\r\nThis package requires a Supabase edge function and database tables to handle WhatsApp OTP functionality. Follow these steps to set up your Supabase project:\r\n\r\n### 1. Create Database Tables\r\n\r\nRun these SQL commands in your Supabase SQL editor:\r\n\r\n#### Complete Database Schema\r\n\r\nRun this complete SQL script in your Supabase SQL editor to set up all required tables, indexes, and security policies:\r\n\r\n```sql\r\n-- Set up database configuration\r\nSET statement_timeout = 0;\r\nSET lock_timeout = 0;\r\nSET idle_in_transaction_session_timeout = 0;\r\nSET client_encoding = 'UTF8';\r\nSET standard_conforming_strings = on;\r\nSELECT pg_catalog.set_config('search_path', '', false);\r\nSET check_function_bodies = false;\r\nSET xmloption = content;\r\nSET client_min_messages = warning;\r\nSET row_security = off;\r\n\r\n-- Create extensions if they don't exist\r\nCREATE EXTENSION IF NOT EXISTS \"pg_graphql\" WITH SCHEMA \"graphql\";\r\nCREATE EXTENSION IF NOT EXISTS \"pg_stat_statements\" WITH SCHEMA \"extensions\";\r\nCREATE EXTENSION IF NOT EXISTS \"pgcrypto\" WITH SCHEMA \"extensions\";\r\nCREATE EXTENSION IF NOT EXISTS \"supabase_vault\" WITH SCHEMA \"vault\";\r\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\" WITH SCHEMA \"extensions\";\r\n\r\n-- Create utility function for updating timestamps\r\nCREATE OR REPLACE FUNCTION \"public\".\"update_updated_at_column\"()\r\nRETURNS \"trigger\"\r\nLANGUAGE \"plpgsql\"\r\nSET \"search_path\" TO ''\r\nAS $$\r\nBEGIN\r\n  NEW.updated_at = now();\r\n  RETURN NEW;\r\nEND;\r\n$$;\r\n\r\n-- Create OTP codes table\r\nCREATE TABLE IF NOT EXISTS \"public\".\"otp_codes\" (\r\n  \"id\" \"uuid\" DEFAULT \"gen_random_uuid\"() NOT NULL,\r\n  \"phone_number\" \"text\" NOT NULL,\r\n  \"code\" \"text\" NOT NULL,\r\n  \"expires_at\" timestamp with time zone NOT NULL,\r\n  \"used\" boolean DEFAULT false NOT NULL,\r\n  \"created_at\" timestamp with time zone DEFAULT \"now\"() NOT NULL,\r\n  \"attempts_left\" integer DEFAULT 3 NOT NULL,\r\n  \"suspended_until\" timestamp with time zone,\r\n  \"suspension_reason\" \"text\"\r\n);\r\n\r\n-- Add table comment\r\nCOMMENT ON COLUMN \"public\".\"otp_codes\".\"expires_at\" IS 'OTP codes should expire within 2-3 minutes for security';\r\n\r\n-- Create primary key\r\nALTER TABLE ONLY \"public\".\"otp_codes\"\r\nADD CONSTRAINT \"otp_codes_pkey\" PRIMARY KEY (\"id\");\r\n\r\n-- Create indexes for better performance\r\nCREATE INDEX \"idx_otp_codes_expires_at\" ON \"public\".\"otp_codes\" USING \"btree\" (\"expires_at\");\r\nCREATE INDEX \"idx_otp_codes_phone_number\" ON \"public\".\"otp_codes\" USING \"btree\" (\"phone_number\");\r\nCREATE INDEX \"idx_otp_codes_phone_suspended\" ON \"public\".\"otp_codes\" USING \"btree\" (\"phone_number\", \"suspended_until\");\r\n\r\n-- Create Row Level Security policies\r\nCREATE POLICY \"Anyone can insert OTP codes\" ON \"public\".\"otp_codes\" FOR INSERT WITH CHECK (true);\r\nCREATE POLICY \"Anyone can select OTP codes\" ON \"public\".\"otp_codes\" FOR SELECT USING (true);\r\nCREATE POLICY \"Anyone can update OTP codes\" ON \"public\".\"otp_codes\" FOR UPDATE USING (true);\r\n\r\n-- Enable Row Level Security\r\nALTER TABLE \"public\".\"otp_codes\" ENABLE ROW LEVEL SECURITY;\r\n\r\n-- Grant necessary permissions\r\nGRANT ALL ON TABLE \"public\".\"otp_codes\" TO \"anon\";\r\nGRANT ALL ON TABLE \"public\".\"otp_codes\" TO \"authenticated\";\r\nGRANT ALL ON TABLE \"public\".\"otp_codes\" TO \"service_role\";\r\nGRANT ALL ON FUNCTION \"public\".\"update_updated_at_column\"() TO \"anon\";\r\nGRANT ALL ON FUNCTION \"public\".\"update_updated_at_column\"() TO \"authenticated\";\r\nGRANT ALL ON FUNCTION \"public\".\"update_updated_at_column\"() TO \"service_role\";\r\n```\r\n\r\n### 2. Deploy the Edge Function\r\n\r\n#### Step 1: Install Supabase CLI\r\n\r\n```bash\r\nnpm install -g supabase\r\n```\r\n\r\n#### Step 2: Login to Supabase\r\n\r\n```bash\r\nsupabase login\r\n```\r\n\r\n#### Step 3: Initialize Supabase in your project (if not already done)\r\n\r\n```bash\r\nsupabase init\r\n```\r\n\r\n#### Step 4: Create the edge function\r\n\r\n```bash\r\nsupabase functions new otp_whatsapp\r\n```\r\n\r\n#### Step 5: Replace the function code\r\n\r\nReplace the contents of `supabase/functions/otp_whatsapp/index.ts` with:\r\n\r\n```typescript\r\nimport { serve } from \"https://deno.land/std@0.168.0/http/server.ts\";\r\nimport { createClient } from \"https://esm.sh/@supabase/supabase-js@2\";\r\n\r\nconst corsHeaders = {\r\n  \"Access-Control-Allow-Origin\": \"*\",\r\n  \"Access-Control-Allow-Headers\":\r\n    \"authorization, x-client-info, apikey, content-type\",\r\n};\r\n\r\nserve(async (req) => {\r\n  // Handle CORS preflight requests\r\n  if (req.method === \"OPTIONS\") {\r\n    return new Response(null, { headers: corsHeaders });\r\n  }\r\n\r\n  try {\r\n    const supabase = createClient(\r\n      Deno.env.get(\"SUPABASE_URL\") ?? \"\",\r\n      Deno.env.get(\"SUPABASE_ANON_KEY\") ?? \"\"\r\n    );\r\n\r\n    // Create admin client for user creation\r\n    const supabaseAdmin = createClient(\r\n      Deno.env.get(\"SUPABASE_URL\") ?? \"\",\r\n      Deno.env.get(\"SUPABASE_SERVICE_ROLE_KEY\") ?? \"\"\r\n    );\r\n\r\n    const { action, phone_number, code } = await req.json();\r\n\r\n    if (action === \"send_otp\") {\r\n      // Check if phone number is currently suspended\r\n      const { data: suspensionCheck } = await supabase\r\n        .from(\"otp_codes\")\r\n        .select(\"suspended_until, suspension_reason\")\r\n        .eq(\"phone_number\", phone_number)\r\n        .not(\"suspended_until\", \"is\", null)\r\n        .gt(\"suspended_until\", new Date().toISOString())\r\n        .order(\"suspended_until\", { ascending: false })\r\n        .limit(1);\r\n\r\n      if (suspensionCheck && suspensionCheck.length > 0) {\r\n        const suspension = suspensionCheck[0];\r\n        const remainingTime = Math.ceil(\r\n          (new Date(suspension.suspended_until).getTime() -\r\n            new Date().getTime()) /\r\n            1000\r\n        );\r\n        const minutes = Math.ceil(remainingTime / 60);\r\n\r\n        return new Response(\r\n          JSON.stringify({\r\n            error: `Phone number is temporarily suspended. Please try again in ${minutes} minute${\r\n              minutes !== 1 ? \"s\" : \"\"\r\n            }.`,\r\n          }),\r\n          {\r\n            status: 429,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      // Generate 6-digit OTP\r\n      const otpCode = Math.floor(100000 + Math.random() * 900000).toString();\r\n      const expiresAt = new Date();\r\n      expiresAt.setMinutes(expiresAt.getMinutes() + 2); // 2 minutes expiry\r\n\r\n      console.log(`Generating OTP for phone ${phone_number}: ${otpCode}`);\r\n\r\n      // Store OTP in database\r\n      const { error: otpError } = await supabase.from(\"otp_codes\").insert({\r\n        phone_number,\r\n        code: otpCode,\r\n        expires_at: expiresAt.toISOString(),\r\n        suspended_until: null,\r\n        suspension_reason: null,\r\n      });\r\n\r\n      if (otpError) {\r\n        console.error(\"Error storing OTP:\", otpError);\r\n        return new Response(\r\n          JSON.stringify({ error: \"Failed to generate OTP\" }),\r\n          {\r\n            status: 500,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      // Send WhatsApp message using Facebook Graph API\r\n      const facebookToken = Deno.env.get(\"FACEBOOK_ACCESS_TOKEN\");\r\n      const phoneNumberId = Deno.env.get(\"WHATSAPP_PHONE_NUMBER_ID\");\r\n\r\n      const whatsappPayload = {\r\n        messaging_product: \"whatsapp\",\r\n        to: phone_number,\r\n        type: \"template\",\r\n        template: {\r\n          name: \"otp_verification_code\",\r\n          language: { code: \"en_US\" },\r\n          components: [\r\n            {\r\n              type: \"BODY\",\r\n              parameters: [{ type: \"text\", text: otpCode }],\r\n            },\r\n            {\r\n              type: \"BUTTON\",\r\n              sub_type: \"URL\",\r\n              index: \"0\",\r\n              parameters: [{ type: \"text\", text: otpCode }],\r\n            },\r\n          ],\r\n        },\r\n      };\r\n\r\n      const whatsappResponse = await fetch(\r\n        `https://graph.facebook.com/v22.0/${phoneNumberId}/messages`,\r\n        {\r\n          method: \"POST\",\r\n          headers: {\r\n            Authorization: `Bearer ${facebookToken}`,\r\n            \"Content-Type\": \"application/json\",\r\n          },\r\n          body: JSON.stringify(whatsappPayload),\r\n        }\r\n      );\r\n\r\n      const whatsappResult = await whatsappResponse.json();\r\n\r\n      if (!whatsappResponse.ok) {\r\n        console.error(\"WhatsApp API error:\", whatsappResult);\r\n        return new Response(\r\n          JSON.stringify({\r\n            error: \"Failed to send WhatsApp message\",\r\n            details: whatsappResult,\r\n          }),\r\n          {\r\n            status: 500,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      return new Response(\r\n        JSON.stringify({\r\n          success: true,\r\n          message: \"OTP sent successfully\",\r\n          message_id: whatsappResult.messages?.[0]?.id,\r\n        }),\r\n        {\r\n          headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n        }\r\n      );\r\n    } else if (action === \"verify_otp\") {\r\n      // Check if phone number is currently suspended\r\n      const { data: suspensionCheck } = await supabase\r\n        .from(\"otp_codes\")\r\n        .select(\"suspended_until, suspension_reason\")\r\n        .eq(\"phone_number\", phone_number)\r\n        .not(\"suspended_until\", \"is\", null)\r\n        .gt(\"suspended_until\", new Date().toISOString())\r\n        .order(\"suspended_until\", { ascending: false })\r\n        .limit(1);\r\n\r\n      if (suspensionCheck && suspensionCheck.length > 0) {\r\n        const suspension = suspensionCheck[0];\r\n        const remainingTime = Math.ceil(\r\n          (new Date(suspension.suspended_until).getTime() -\r\n            new Date().getTime()) /\r\n            1000\r\n        );\r\n        const minutes = Math.ceil(remainingTime / 60);\r\n\r\n        return new Response(\r\n          JSON.stringify({\r\n            error: `Phone number is temporarily suspended. Please try again in ${minutes} minute${\r\n              minutes !== 1 ? \"s\" : \"\"\r\n            }.`,\r\n          }),\r\n          {\r\n            status: 429,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      // Check if there's a valid OTP for this phone number\r\n      const { data: otpRecord, error: fetchError } = await supabase\r\n        .from(\"otp_codes\")\r\n        .select(\"*\")\r\n        .eq(\"phone_number\", phone_number)\r\n        .eq(\"used\", false)\r\n        .gt(\"expires_at\", new Date().toISOString())\r\n        .order(\"created_at\", { ascending: false })\r\n        .limit(1)\r\n        .maybeSingle();\r\n\r\n      if (fetchError) {\r\n        console.error(\"Error fetching OTP:\", fetchError);\r\n        return new Response(JSON.stringify({ error: \"Verification failed\" }), {\r\n          status: 500,\r\n          headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n        });\r\n      }\r\n\r\n      if (!otpRecord) {\r\n        return new Response(\r\n          JSON.stringify({ error: \"Invalid or expired OTP code\" }),\r\n          {\r\n            status: 400,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      // Check if the code matches\r\n      if (otpRecord.code !== code) {\r\n        const newAttemptsLeft = otpRecord.attempts_left - 1;\r\n\r\n        await supabase\r\n          .from(\"otp_codes\")\r\n          .update({ attempts_left: newAttemptsLeft })\r\n          .eq(\"id\", otpRecord.id);\r\n\r\n        if (newAttemptsLeft <= 0) {\r\n          // Suspend the phone number for 3 minutes\r\n          const suspendedUntil = new Date();\r\n          suspendedUntil.setMinutes(suspendedUntil.getMinutes() + 3);\r\n\r\n          await supabase\r\n            .from(\"otp_codes\")\r\n            .update({\r\n              used: true,\r\n              suspended_until: suspendedUntil.toISOString(),\r\n              suspension_reason: \"max_attempts_reached\",\r\n            })\r\n            .eq(\"id\", otpRecord.id);\r\n\r\n          return new Response(\r\n            JSON.stringify({\r\n              error:\r\n                \"Maximum attempts reached. Your phone number has been temporarily suspended for 3 minutes.\",\r\n            }),\r\n            {\r\n              status: 429,\r\n              headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n            }\r\n          );\r\n        }\r\n\r\n        return new Response(\r\n          JSON.stringify({\r\n            error: `Invalid OTP code. ${newAttemptsLeft} attempts remaining.`,\r\n          }),\r\n          {\r\n            status: 400,\r\n            headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n          }\r\n        );\r\n      }\r\n\r\n      // Mark OTP as used\r\n      const { error: updateError } = await supabase\r\n        .from(\"otp_codes\")\r\n        .update({ used: true })\r\n        .eq(\"id\", otpRecord.id);\r\n\r\n      if (updateError) {\r\n        console.error(\"Error updating OTP:\", updateError);\r\n        return new Response(JSON.stringify({ error: \"Verification failed\" }), {\r\n          status: 500,\r\n          headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n        });\r\n      }\r\n\r\n      // Handle user authentication\r\n      const { data: existingUsers } =\r\n        await supabaseAdmin.auth.admin.listUsers();\r\n      const existingUser = existingUsers.users?.find(\r\n        (u) => u.phone === phone_number\r\n      );\r\n\r\n      let user;\r\n      if (existingUser) {\r\n        user = existingUser;\r\n      } else {\r\n        // Create new user with phone number\r\n        const { data: newUserData, error: createError } =\r\n          await supabaseAdmin.auth.admin.createUser({\r\n            phone: phone_number,\r\n            phone_confirm: true,\r\n            email_confirm: true,\r\n            user_metadata: {\r\n              phone: phone_number,\r\n              provider: \"phone\",\r\n            },\r\n          });\r\n\r\n        if (createError) {\r\n          console.error(\"Error creating user:\", createError);\r\n          return new Response(\r\n            JSON.stringify({ error: \"Failed to create user account\" }),\r\n            {\r\n              status: 500,\r\n              headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n            }\r\n          );\r\n        }\r\n\r\n        user = newUserData.user;\r\n      }\r\n\r\n      // Send success template\r\n      const facebookToken = Deno.env.get(\"FACEBOOK_ACCESS_TOKEN\");\r\n      const phoneNumberId = Deno.env.get(\"WHATSAPP_PHONE_NUMBER_ID\");\r\n\r\n      const successPayload = {\r\n        messaging_product: \"whatsapp\",\r\n        to: phone_number,\r\n        type: \"template\",\r\n        template: {\r\n          name: \"otp_success\",\r\n          language: { code: \"en_US\" },\r\n        },\r\n      };\r\n\r\n      await fetch(\r\n        `https://graph.facebook.com/v22.0/${phoneNumberId}/messages`,\r\n        {\r\n          method: \"POST\",\r\n          headers: {\r\n            Authorization: `Bearer ${facebookToken}`,\r\n            \"Content-Type\": \"application/json\",\r\n          },\r\n          body: JSON.stringify(successPayload),\r\n        }\r\n      );\r\n\r\n      return new Response(\r\n        JSON.stringify({\r\n          success: true,\r\n          message: \"Phone number verified and user authenticated successfully\",\r\n          verified: true,\r\n          user: user,\r\n        }),\r\n        {\r\n          headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n        }\r\n      );\r\n    }\r\n\r\n    return new Response(JSON.stringify({ error: \"Invalid action\" }), {\r\n      status: 400,\r\n      headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n    });\r\n  } catch (error) {\r\n    console.error(\"Error in otp_whatsapp function:\", error);\r\n    return new Response(JSON.stringify({ error: error.message }), {\r\n      status: 500,\r\n      headers: { ...corsHeaders, \"Content-Type\": \"application/json\" },\r\n    });\r\n  }\r\n});\r\n```\r\n\r\n#### Step 6: Set up environment variables\r\n\r\n```bash\r\n# Set your environment variables\r\nsupabase secrets set FACEBOOK_ACCESS_TOKEN=your_facebook_access_token\r\n```\r\n\r\n#### Step 7: Deploy the function\r\n\r\n```bash\r\nsupabase functions deploy otp_whatsapp\r\n```\r\n\r\n### 3. WhatsApp Business API Setup\r\n\r\n1. **Create a Meta Business Account** at https://business.facebook.com\r\n2. **Set up WhatsApp Business API** through Meta Business\r\n3. **Create message templates** in your WhatsApp Business Manager:\r\n   - Template name: `otp_verification_code`\r\n   - Template name: `otp_success`\r\n4. **Get your access token** and phone number ID from the Meta Developer Console\r\n5. **Configure webhooks** (optional) for delivery status\r\n\r\n### 4. Update Your App Configuration\r\n\r\nMake sure your app configuration points to your Supabase project:\r\n\r\n```tsx\r\nconst config = {\r\n  supabaseUrl: \"https://your-project.supabase.co\",\r\n  supabaseKey: \"your-anon-key\", // This should be your anon/public key\r\n};\r\n```\r\n\r\n### 5. Test Your Setup\r\n\r\nYou can test your edge function directly:\r\n\r\n```bash\r\ncurl -X POST 'https://your-project.supabase.co/functions/v1/otp_whatsapp' \\\r\n  -H 'Authorization: Bearer YOUR_ANON_KEY' \\\r\n  -H 'Content-Type: application/json' \\\r\n  -d '{\r\n    \"action\": \"send_otp\",\r\n    \"phone_number\": \"+1234567890\"\r\n  }'\r\n```\r\n\r\n## License\r\n\r\nMIT\r\n\r\n## Support\r\n\r\nFor issues and questions, please visit our [GitHub repository](https://github.com/gebeya/gebeya-whatsapp-otp).\r\n\r\n## Development\r\n\r\n### Package Validation\r\n\r\nThis package includes a comprehensive validation script to ensure the build is correct and ready for publishing. The validation script tests:\r\n\r\n- Build file existence and formats\r\n- Package.json configuration\r\n- Import functionality in both JavaScript and TypeScript\r\n- Peer dependency resolution\r\n- Basic component functionality\r\n\r\n#### Running Validation\r\n\r\n```bash\r\n# Run the full package validation\r\nnpm run validate\r\n\r\n# Run just the build validation\r\nnpm run validate-build\r\n```\r\n\r\n#### What Gets Validated\r\n\r\n1. **Build Files**: Checks that all required build outputs exist\r\n\r\n   - `dist/index.cjs` (CommonJS build)\r\n   - `dist/index.esm.js` (ES modules build)\r\n   - `dist/index.d.ts` (TypeScript declarations)\r\n   - Component-specific declaration files\r\n\r\n2. **Package Configuration**: Validates package.json settings\r\n\r\n   - Entry points (main, module, types)\r\n   - Peer dependencies configuration\r\n   - File inclusion settings\r\n\r\n3. **Import Testing**: Tests imports in different environments\r\n\r\n   - CommonJS require() syntax\r\n   - ES modules import syntax\r\n   - TypeScript type checking and compilation\r\n\r\n4. **Peer Dependencies**: Verifies React is properly externalized\r\n\r\n   - Checks that React is not bundled in the output\r\n   - Validates peer dependency resolution\r\n\r\n5. **Component Functionality**: Basic validation of exported components\r\n   - Ensures all exports are available\r\n   - Validates component types and structure\r\n\r\n#### Validation Output\r\n\r\nThe validation script provides detailed feedback:\r\n\r\n```\r\n🔍 Comprehensive Package Validation for gebeya-whatsapp-otp\r\n\r\n1. Checking build output files...\r\n   ✓ dist/index.cjs exists\r\n   ✓ dist/index.esm.js exists\r\n   ✓ dist/index.d.ts exists\r\n   ...\r\n\r\n✅ Package validation PASSED\r\n📦 Package is ready for publishing\r\n```\r\n\r\n### Building the Package\r\n\r\n```bash\r\n# Build the package\r\nnpm run build\r\n\r\n# Build and validate\r\nnpm run build && npm run validate\r\n```\r\n\r\nThe build process:\r\n\r\n1. Compiles TypeScript to generate declaration files\r\n2. Bundles with Rollup to create CommonJS and ES module outputs\r\n3. Generates source maps for debugging\r\n4. Excludes peer dependencies from the bundle\r\n","readmeFilename":"README.md"}