Skip to content

md-lotfi/gatewire-react

Repository files navigation

gatewire-react

npm version CI License: MIT Coverage

React hooks and TypeScript types for integrating the Wiregate OTP verification flow into any React or Next.js application. Zero runtime dependencies.


Security note

Your Wiregate API key must never be embedded in client-side JavaScript.

gatewire-react never calls the Wiregate API directly. Instead, it accepts adapter functions that call your own backend proxy, which holds the real API key server-side. This is a hard architectural constraint — see Setting up your proxy for ready-to-use examples.

Browser (gatewire-react)
      │
      │  POST /api/otp/send  (no API key)
      ▼
Your Backend Proxy  ──── POST https://gatewire.raystate.com/api/v1/send-otp  (API key here)
                                       │
                                       ▼
                                 Wiregate API

Installation

npm install gatewire-react
# or
yarn add gatewire-react
# or
pnpm add gatewire-react

Peer dependency: React ≥ 17


Quick start

import { useOtp, createFetchAdapter } from 'gatewire-react';

// Point to your own backend proxy — never the Wiregate API directly
const adapter = createFetchAdapter('/api/otp');

function PhoneVerification() {
  const [phone, setPhone] = React.useState('');
  const [code, setCode] = React.useState('');

  const { state, error, sendOtp, verifyOtp, resend, resendCountdown } = useOtp({
    adapter,
    resendCooldown: 60,   // seconds before user can resend
    pollInterval: 3000,   // ms between status polls (0 to disable)
  });

  if (state === 'verified') return <p>Phone verified!</p>;

  if (state === 'idle' || state === 'sending') {
    return (
      <form onSubmit={e => { e.preventDefault(); sendOtp(phone); }}>
        <input
          type="tel"
          value={phone}
          onChange={e => setPhone(e.target.value)}
          placeholder="+213770123456"
        />
        <button type="submit" disabled={state === 'sending'}>
          {state === 'sending' ? 'Sending…' : 'Send code'}
        </button>
        {error && <p role="alert">{error}</p>}
      </form>
    );
  }

  return (
    <form onSubmit={e => { e.preventDefault(); verifyOtp(code); }}>
      <input
        type="text"
        value={code}
        onChange={e => setCode(e.target.value)}
        placeholder="6-digit code"
        maxLength={6}
      />
      <button type="submit" disabled={state === 'verifying'}>
        {state === 'verifying' ? 'Verifying…' : 'Verify'}
      </button>
      <button type="button" onClick={resend} disabled={resendCountdown > 0}>
        {resendCountdown > 0 ? `Resend in ${resendCountdown}s` : 'Resend code'}
      </button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

How it works

useOtp({ adapter, pollInterval, resendCooldown })
  │
  ├── sendOtp(phone)      → calls adapter.sendOtp()  → POST /api/otp/send
  ├── verifyOtp(code)     → calls adapter.verifyOtp() → POST /api/otp/verify
  ├── resend()            → alias for sendOtp() with last phone (respects cooldown)
  ├── reset()             → resets all state
  └── (internal polling)  → calls adapter.getStatus() → GET /api/otp/status/:id

OTP state machine:

  idle
    │  sendOtp(phone)
    ▼
  sending  ──(error)──▶  idle (error set)
    │
    ▼
  pending  ──(poll)──▶  dispatched ──▶  sent
    │                                    │
    │                    ┌───────────────┤
    │                    ▼               ▼
    │                  failed         expired / cancelled
    │
    │  verifyOtp(code)
    ▼
  verifying ──(error)──▶ sent (error set, user can retry)
    │
    ▼
  verified  (terminal)

API reference

useOtp(options)

The main hook for the full OTP lifecycle.

function useOtp(options: UseOtpOptions): UseOtpReturn

UseOtpOptions

Property Type Default Description
adapter GatewireAdapter required Object with sendOtp, verifyOtp, and optional getStatus functions
pollInterval number 3000 Status poll interval in ms. Set to 0 to disable polling
resendCooldown number 60 Seconds the user must wait before resending

UseOtpReturn

Property Type Description
state OtpStatus Current lifecycle state
referenceId string | null Set after a successful sendOtp call
error string | null Human-readable error from the last failed operation
isLoading boolean true while sendOtp or verifyOtp is in flight
resendCountdown number Seconds remaining in the resend cooldown
sendOtp(phone, templateKey?) () => Promise<void> Send an OTP to the given phone number
verifyOtp(code) () => Promise<void> Verify the code the user entered
resend() () => Promise<void> Resend to the last phone (no-op if cooldown is active)
reset() () => void Reset all state to initial values

OtpStatus values: 'idle' | 'sending' | 'pending' | 'dispatched' | 'sent' | 'verifying' | 'verified' | 'failed' | 'expired' | 'cancelled'


useOtpStatus(referenceId, getStatus, options?)

Lower-level composable hook for polling the OTP status independently.

function useOtpStatus(
  referenceId: string | null,
  getStatus: GatewireAdapter['getStatus'],
  options?: { interval?: number; enabled?: boolean }
): { status: OtpStatus | null; error: string | null; isLoading: boolean }

Polls getStatus every interval ms (default 3000) while enabled !== false and referenceId is non-null. Automatically stops when a terminal status (sent, verified, failed, expired, cancelled) is received.


createFetchAdapter(proxyBaseUrl)

Creates a GatewireAdapter using the native fetch API that calls your backend proxy.

function createFetchAdapter(proxyBaseUrl: string): GatewireAdapter

Expected routes on your proxy:

Method Path Forwards to
POST {proxyBaseUrl}/send Wiregate send-otp
POST {proxyBaseUrl}/verify Wiregate verify-otp
GET {proxyBaseUrl}/status/:id Wiregate status

Example:

// All calls go to /api/otp/send, /api/otp/verify, /api/otp/status/:id
const adapter = createFetchAdapter('/api/otp');

GatewireError

Thrown by createFetchAdapter when the proxy returns a non-2xx response.

class GatewireError extends Error {
  readonly statusCode: number | undefined;
  readonly body: unknown;
}

Catch it to display user-friendly error messages:

import { GatewireError } from 'gatewire-react';

try {
  await adapter.sendOtp(phone);
} catch (err) {
  if (err instanceof GatewireError) {
    console.error(err.message, err.statusCode, err.body);
  }
}

Note: useOtp catches GatewireError automatically and sets the error field. Only non-GatewireError exceptions are re-thrown.


Custom adapter

You can write your own adapter instead of using createFetchAdapter:

import type { GatewireAdapter } from 'gatewire-react';

const adapter: GatewireAdapter = {
  async sendOtp(phone, templateKey) {
    const res = await myApiClient.post('/otp/send', { phone, template_key: templateKey });
    return res.data; // { reference_id, status: 'pending' }
  },
  async verifyOtp(referenceId, code) {
    const res = await myApiClient.post('/otp/verify', { reference_id: referenceId, code });
    return res.data; // { status: 'verified', message }
  },
  async getStatus(referenceId) {
    const res = await myApiClient.get(`/otp/status/${referenceId}`);
    return res.data; // { reference_id, status, created_at }
  },
};

Setting up your proxy

Your proxy server is the only place that holds the Wiregate API key. It receives requests from gatewire-react and forwards them to the Wiregate API.

Express / Node.js example

import express from 'express';
import { GateWireClient } from 'gatewire'; // Node.js SDK

const gw = new GateWireClient({ apiKey: process.env.GATEWIRE_API_KEY! });
const router = express.Router();

router.post('/otp/send', async (req, res) => {
  try {
    const { phone, template_key } = req.body as { phone: string; template_key?: string };
    const result = await gw.otp.send({ phone, templateKey: template_key });
    res.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to send OTP';
    res.status(422).json({ error: message });
  }
});

router.post('/otp/verify', async (req, res) => {
  try {
    const { reference_id, code } = req.body as { reference_id: string; code: string };
    const result = await gw.otp.verify({ referenceId: reference_id, code });
    res.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Verification failed';
    res.status(400).json({ error: message });
  }
});

router.get('/otp/status/:id', async (req, res) => {
  try {
    const result = await gw.otp.getStatus(req.params.id!);
    res.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Status check failed';
    res.status(500).json({ error: message });
  }
});

export default router;

Next.js Route Handler example

// app/api/otp/send/route.ts
import { GateWireClient } from 'gatewire';

const gw = new GateWireClient({ apiKey: process.env.GATEWIRE_API_KEY! });

export async function POST(request: Request) {
  try {
    const { phone, template_key } = (await request.json()) as {
      phone: string;
      template_key?: string;
    };
    const result = await gw.otp.send({ phone, templateKey: template_key });
    return Response.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to send OTP';
    return Response.json({ error: message }, { status: 422 });
  }
}
// app/api/otp/verify/route.ts
import { GateWireClient } from 'gatewire';

const gw = new GateWireClient({ apiKey: process.env.GATEWIRE_API_KEY! });

export async function POST(request: Request) {
  try {
    const { reference_id, code } = (await request.json()) as {
      reference_id: string;
      code: string;
    };
    const result = await gw.otp.verify({ referenceId: reference_id, code });
    return Response.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Verification failed';
    return Response.json({ error: message }, { status: 400 });
  }
}
// app/api/otp/status/[id]/route.ts
import { GateWireClient } from 'gatewire';

const gw = new GateWireClient({ apiKey: process.env.GATEWIRE_API_KEY! });

export async function GET(_request: Request, { params }: { params: { id: string } }) {
  try {
    const result = await gw.otp.getStatus(params.id);
    return Response.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Status check failed';
    return Response.json({ error: message }, { status: 500 });
  }
}

Then point the adapter at your proxy:

// In your React app — note: this is a relative URL, not the Wiregate API
const adapter = createFetchAdapter('/api/otp');

TypeScript

All types are exported from the package root:

import type {
  OtpStatus,
  GatewireAdapter,
  UseOtpOptions,
  UseOtpReturn,
  SendOtpResult,
  VerifyOtpResult,
  StatusResult,
} from 'gatewire-react';

The library is written in TypeScript with strict: true and ships declaration files (.d.ts) for full type safety.


Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Install dependencies: npm install
  4. Make your changes, ensuring tests pass: npm test
  5. Check types: npm run typecheck
  6. Submit a pull request

Please follow the existing code style and add tests for any new behaviour.


License

MIT © GateWire Team

About

Gatewire react version

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors