# @quantaroute/checkout

> **DigiPin-powered smart address checkout widget for Indian e-commerce.**
> Two-step: Map pin → Auto-filled form.
> One package. Three platforms: **React web · iOS · Android.**

[Made in India](https://quantaroute.com)
[DigiPin](https://www.indiapost.gov.in)
[Map](https://carto.com)
[License](./LICENSE)

---

## What it does

Replaces broken Indian address forms with a precise two-step pin-drop flow:

```
Step 1 – Map Pin                    Step 2 – Auto-fill + Details
┌──────────────────────────┐        ┌──────────────────────────┐
│  [DigiPin: 39J-438-TJC7] │        │  📍 Auto-detected         │
│                           │        │  State:    Delhi          │
│    🗺  OSM vector map     │  ──►  │  District: New Delhi     │
│         📍 ← drag         │        │  Pincode:  110011         │
│                           │        │  ✓ Deliverable            │
│  [⊕ Locate Me]            │        │  🏠 Add details           │
│  [Confirm Location →]     │        │  Flat No:  [_________]    │
└──────────────────────────┘        │  [← Adjust] [Save ✓]    │
                                     └──────────────────────────┘
```

**Key features:**

- DigiPin shown **offline** in real-time as the user drags the pin (~4 m × 4 m precision)
- **No Google Maps.** Free Carto Positron vector basemap
- Auto-fills State, District, Locality, Pincode, Delivery status from QuantaRoute API
- Manual fields: Flat no., Floor, Building (OSM pre-filled), Street/Area (OSM pre-filled)
- Mobile-first (full-screen on phones, card on desktop/tablet)
- Dark mode, reduced-motion, keyboard navigation, ARIA labels
- TypeScript — zero runtime deps beyond peer dependencies

---

## Platform support


| Platform                      | Bundler        | Map engine                        | Install                      |
| ----------------------------- | -------------- | --------------------------------- | ---------------------------- |
| React / Next.js / Vite / Nuxt | Webpack / Vite | MapLibre GL JS                    | `maplibre-gl`                |
| iOS (Expo / React Native)     | Metro          | expo-osm-sdk (MapLibre GL Native) | `expo-osm-sdk expo-location` |
| Android (Expo / React Native) | Metro          | expo-osm-sdk (MapLibre GL Native) | `expo-osm-sdk expo-location` |


> **Same import on all platforms.**
> Metro automatically resolves `.native.tsx` files on mobile; Vite/Webpack use the web `.tsx` files.

---

## Quick start

### 0 · Get an API key

1. Sign up at **[developers.quantaroute.com](https://developers.quantaroute.com)**
2. Create a project → copy your **API key**

> **Never hard-code or commit API keys to git.**

---

## Web (React / Next.js / Vite / Nuxt)

### Install

```bash
npm install @quantaroute/checkout maplibre-gl
```

### Import CSS

```ts
// In your app entry file (main.tsx / _app.tsx / layout.tsx / nuxt.config.ts)
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
```

### Use

```tsx
import { CheckoutWidget } from '@quantaroute/checkout';

export default function CheckoutPage() {
  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      onComplete={(address) => {
        console.log(address.digipin);           // "39J-438-TJC7"
        console.log(address.pincode);           // "110011"
        console.log(address.formattedAddress);  // "Flat 4B, Floor 3rd, ..."
        // → send to your backend / payment gateway
      }}
    />
  );
}
```

### Next.js (App Router)

```tsx
// app/checkout/page.tsx
'use client';

import dynamic from 'next/dynamic';

const CheckoutWidget = dynamic(
  () => import('@quantaroute/checkout').then((m) => m.CheckoutWidget),
  { ssr: false }
);

export default function CheckoutPage() {
  return (
    <main className="max-w-lg mx-auto p-4">
      <CheckoutWidget
        apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
        onComplete={(addr) => console.log(addr)}
      />
    </main>
  );
}
```

CSS in `app/layout.tsx`:

```tsx
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
```

### Next.js (Pages Router)

```tsx
import dynamic from 'next/dynamic';
const CheckoutWidget = dynamic(() => import('@quantaroute/checkout'), { ssr: false });

export default function CheckoutPage() {
  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      onComplete={(a) => console.log(a)}
    />
  );
}
```

### Nuxt 3

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  css: ['maplibre-gl/dist/maplibre-gl.css', '@quantaroute/checkout/style.css'],
});
```

```vue
<!-- components/AddressWidget.client.vue  (.client = browser-only) -->
<script setup lang="ts">
import { CheckoutWidget } from '@quantaroute/checkout';
const { public: { qrApiKey } } = useRuntimeConfig();
</script>
<template>
  <CheckoutWidget :api-key="qrApiKey" map-height="360px" @complete="console.log" />
</template>
```

### Vite + React

```tsx
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
import { CheckoutWidget } from '@quantaroute/checkout';

function App() {
  return (
    <CheckoutWidget
      apiKey={import.meta.env.VITE_QR_API_KEY}
      onComplete={(addr) => console.log('Saved:', addr)}
    />
  );
}
```

### Vanilla JS (UMD / script tag)

```html
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css" />
<link rel="stylesheet" href="https://unpkg.com/@quantaroute/checkout/dist/style.css" />

<div id="checkout-root"></div>

<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/@quantaroute/checkout/dist/lib/quantaroute-checkout.umd.js"></script>
<script>
  ReactDOM.createRoot(document.getElementById('checkout-root')).render(
    React.createElement(QuantaRouteCheckout.CheckoutWidget, {
      apiKey: 'YOUR_KEY',
      onComplete: (addr) => console.log('Done:', addr),
    })
  );
</script>
```

---

## Native (Expo / React Native — iOS & Android)

> **Requires a [development build](https://docs.expo.dev/develop/development-builds/introduction/).**
> This does NOT work in Expo Go — `expo-osm-sdk` uses native modules.

### 1 · Install

```bash
npm install @quantaroute/checkout expo-osm-sdk expo-location
```

### 2 · Add the config plugin to `app.json`

```json
{
  "expo": {
    "plugins": [
      ["@quantaroute/checkout/plugin", {
        "locationPermissionText": "Allow access to your location to place the delivery pin on the map."
      }]
    ]
  }
}
```

This single plugin automatically configures:


| What                | iOS                                   | Android                |
| ------------------- | ------------------------------------- | ---------------------- |
| MapLibre native SDK | `expo-osm-sdk/plugin`                 | `expo-osm-sdk/plugin`  |
| Location permission | `NSLocationWhenInUseUsageDescription` | `ACCESS_FINE_LOCATION` |
| Internet permission | –                                     | `INTERNET`             |


### 3 · Rebuild your dev client

```bash
npx expo run:ios
# or
npx expo run:android
```

### 4 · Use — identical import as web

```tsx
import { CheckoutWidget } from '@quantaroute/checkout';
// No CSS import — styles are React Native StyleSheet objects

export default function CheckoutScreen() {
  return (
    <CheckoutWidget
      apiKey={process.env.EXPO_PUBLIC_QUANTAROUTE_KEY!}
      mapHeight={380}
      onComplete={(address) => {
        console.log(address.digipin);           // "39J-438-TJC7"
        console.log(address.formattedAddress);  // "Flat 4B, ..."
      }}
    />
  );
}
```

### Demo native app

A working Expo demo is in `demo-native/`:

```bash
cd demo-native
npm install
# Set your API key in .env:  EXPO_PUBLIC_QUANTAROUTE_API_KEY=your_key
npx expo run:ios
npx expo run:android
```

---

## Props

### Core props


| Prop               | Type                                   | Default                       | Platform |
| ------------------ | -------------------------------------- | ----------------------------- | -------- |
| `apiKey`           | `string`                               | **required**                  | all      |
| `onComplete`       | `(addr: CompleteAddress) => void`      | **required**                  | all      |
| `apiBaseUrl`       | `string`                               | `https://api.quantaroute.com` | all      |
| `onError`          | `(err: Error) => void`                 | –                             | all      |
| `defaultLat`       | `number`                               | India center                  | all      |
| `defaultLng`       | `number`                               | India center                  | all      |
| `theme`            | `'light' | 'dark'`                     | `'light'`                     | all      |
| `mapHeight`        | `string | number`                      | `'380px'` / `380`             | all      |
| `title`            | `string`                               | `'Add Delivery Address'`      | all      |
| `className`        | `string`                               | –                             | web only |
| `style`            | `CSSProperties | StyleProp<ViewStyle>` | –                             | all      |
| `indiaBoundaryUrl` | `string`                               | –                             | web only |


### Search bar


| Prop           | Type      | Default | Platform |
| -------------- | --------- | ------- | -------- |
| `enableSearch` | `boolean` | `true`  | all      |


When `enableSearch` is true, a unified search bar appears above the map. It handles two modes automatically:

- **Address search** — calls `/v1/digipin/autocomplete` (debounced 400 ms, location-biased)
- **DigiPin input** — decoded **100 % offline**, zero API cost, instant map flyTo

As India Post promotes DigiPin nationally, buyers can paste their 10-character code and land on their exact ±4 m location.

### Phone OTP + saved addresses (D2C checkout)


| Prop                      | Type      | Default | Platform |
| ------------------------- | --------- | ------- | -------- |
| `enablePhoneAuth`         | `boolean` | `false` | all      |
| `merchantId`              | `string`  | –       | all      |
| `supabaseFunctionBaseUrl` | `string`  | –       | all      |


When `enablePhoneAuth` is true the widget starts with a phone-entry step. Returning buyers can pick a previously saved address from any brand in the network instead of dropping the pin again.

On the address details step, buyers choose a **Home / Work / Other** label before saving. The label is stored in the checkout-addresses API and returned on `CompleteAddress.label`.

**Requires:**

1. A merchant account — sign up at [developers.quantaroute.com/onboarding](https://developers.quantaroute.com/onboarding/merchant) to get your `merchantId` and `supabaseFunctionBaseUrl`.
2. The `checkout-otp` and `checkout-addresses` Edge Functions deployed (see [Supabase setup](#supabase-setup)).

```tsx
<CheckoutWidget
  apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
  merchantId="your-merchant-uuid"
  supabaseFunctionBaseUrl="https://<project>.supabase.co/functions/v1"
  enablePhoneAuth={true}
  enableSearch={true}
  onComplete={(addr) => console.log(addr)}
/>
```

### Email OTP auth (`enableEmailAuth`)


| Prop                      | Type      | Default | Platform |
| ------------------------- | --------- | ------- | -------- |
| `enableEmailAuth`         | `boolean` | `false` | all      |
| `merchantId`              | `string`  | –       | all      |
| `supabaseFunctionBaseUrl` | `string`  | –       | all      |


When `enableEmailAuth` is true the widget starts with an email-entry step. The buyer receives a 6-digit OTP via **Resend**. On success a checkout session is created and saved addresses are shown if available.

> If both `enablePhoneAuth` and `enableEmailAuth` are set, phone OTP takes precedence.

```tsx
<CheckoutWidget
  apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
  merchantId="your-merchant-uuid"
  supabaseFunctionBaseUrl="https://<project>.supabase.co/functions/v1"
  enableEmailAuth={true}
  onComplete={(addr) => console.log(addr)}
  onCheckoutEvent={(event) => {
    // email-specific events: email_step_viewed | email_otp_sent | email_verified
    console.log('[checkout]', event.type, event);
  }}
/>
```

**Client-side events emitted during email auth:**


| Event type          | When                          |
| ------------------- | ----------------------------- |
| `email_step_viewed` | Email input screen rendered   |
| `email_otp_sent`    | OTP dispatched via Resend     |
| `email_verified`    | OTP correct → session created |


---

### Merchant courier serviceability

QuantaRoute still validates that a pincode exists in the India Post database, but India Post "Non Delivery" is treated as **reference data**, not the final merchant delivery decision. Most D2C stores ship through Shiprocket, Delhivery, ClickPost, or direct courier contracts, so the buyer-facing serviceability check should come from the merchant.


| Prop                           | Type      | Default          | Platform |
| ------------------------------ | --------- | ---------------- | -------- |
| `enableServiceabilityCheck`    | `boolean` | `false`          | all      |
| `showIndiaPostDeliveryWarning` | `boolean` | `false`          | all      |
| `serviceabilityBlockCheckout`  | `boolean` | merchant setting | all      |


When `enableServiceabilityCheck` is true and the buyer has a checkout session, the widget calls `checkout-addresses` with `action: "serviceability_check"`. The Edge Function calls the merchant's configured courier endpoint server-side and fails open if that endpoint is unavailable.

```tsx
<CheckoutWidget
  apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
  merchantId={process.env.NEXT_PUBLIC_MERCHANT_ID!}
  supabaseFunctionBaseUrl={process.env.NEXT_PUBLIC_SUPABASE_FUNCTION_URL!}
  enableEmailAuth={true}
  enableServiceabilityCheck={true}
  onComplete={(addr) => console.log(addr)}
/>
```

Merchant configuration lives on `checkout.merchants`:


| Column                             | Meaning                                           |
| ---------------------------------- | ------------------------------------------------- |
| `serviceability_enabled`           | Enables the merchant courier check                |
| `serviceability_webhook_url`       | Merchant endpoint called by QuantaRoute           |
| `serviceability_webhook_secret`    | Shared HMAC secret for outbound calls             |
| `serviceability_block_checkout`    | If true, `serviceable: false` blocks address save |
| `show_india_post_delivery_warning` | Optional soft India Post info banner fallback     |


Merchant endpoint contract:

```http
POST /checkout/serviceability
Content-Type: application/json
X-QuantaRoute-Timestamp: 1782000000000
X-QuantaRoute-Signature: v1=<hmac_sha256(timestamp + "." + body)>
```

```json
{
  "merchant_id": "merchant-uuid",
  "pincode": "110011",
  "lat": 28.6139,
  "lng": 77.209,
  "requested_at": "2026-06-21T00:00:00.000Z"
}
```

Expected response:

```json
{ "serviceable": true, "message": "Delivery available in 3-5 days" }
```

or:

```json
{ "serviceable": false, "message": "We do not currently ship to this pincode" }
```

If the merchant endpoint errors, times out, or returns an invalid response, checkout **fails open** and the buyer can continue. `showIndiaPostDeliveryWarning` is opt-in and only shows a soft informational banner: "Courier partners may still deliver to this pincode."

---

#### Setting up Resend (required for `enableEmailAuth`)

> **Hosted merchants (recommended):** If you onboarded at [developers.quantaroute.com](https://developers.quantaroute.com), QuantaRoute already runs Resend on shared infrastructure. Skip steps 1–5 below — just set `enableEmailAuth={true}` and register your domain in the portal. The steps below apply only when **self-hosting** the `checkout-email-otp` Edge Function in your own Supabase project.

1. **Create a Resend account** at [resend.com](https://resend.com) — free tier covers ~3 000 emails / month.
2. **Verify your sending domain** (e.g. `checkout.yourbrand.com` or `yourbrand.com`):
  - Go to **Resend → Domains → Add domain**
  - Add the DNS records shown (SPF, DKIM, DMARC) to your DNS provider
  - Wait for "Verified" status (usually under 5 minutes with Cloudflare; up to 48 h with others)
3. **Get an API key** — Resend → API Keys → Create. Restrict it to **Send access** only.
4. **Set secrets on the `checkout-email-otp` Edge Function:**
  In [Supabase Dashboard](https://supabase.com/dashboard) → your project → Edge Functions → Secrets:

  | Secret              | Value                                                    |
  | ------------------- | -------------------------------------------------------- |
  | `RESEND_API_KEY`    | `re_...` from Resend                                     |
  | `RESEND_FROM_EMAIL` | A verified sender address, e.g. `checkout@yourbrand.com` |

5. **Deploy the Edge Function** (if not already done):
  ```bash
   supabase functions deploy checkout-email-otp --project-ref <your-project-ref> --no-verify-jwt
  ```

**Required env vars (frontend):**

```env
NEXT_PUBLIC_QUANTAROUTE_KEY=dp_...
NEXT_PUBLIC_MERCHANT_ID=your-uuid-from-developer-portal
NEXT_PUBLIC_SUPABASE_FUNCTION_URL=https://<project-ref>.supabase.co/functions/v1
```

---

#### Per-merchant email template customisation

Set these columns directly on your `checkout.merchants` row (SQL or Supabase Table Editor). All are optional — omitting any falls back to the QuantaRoute default.


| Column                   | Type   | Default                                  | Effect                                                                                        |
| ------------------------ | ------ | ---------------------------------------- | --------------------------------------------------------------------------------------------- |
| `email_from_name`        | `text` | `"{brand} Checkout"`                     | "From" display name in the inbox, e.g. `"Fabindia"`                                           |
| `email_logo_url`         | `text` | *(text brand name)*                      | HTTPS URL of your logo (PNG/SVG, 180 × 48 px recommended) shown at the top of every OTP email |
| `email_subject_template` | `text` | `"{code} is your {brand} checkout code"` | Subject line; supports `{code}` and `{brand}` placeholders                                    |


**Example SQL:**

```sql
UPDATE checkout.merchants
SET
  email_from_name        = 'Fabindia',
  email_logo_url         = 'https://cdn.fabindia.com/logo-email.png',
  email_subject_template = '{code} — verify your Fabindia order'
WHERE id = 'your-merchant-uuid';
```

---

### Branding


| Prop           | Type           | Default   | Platform |
| -------------- | -------------- | --------- | -------- |
| `brandColor`   | `string` (hex) | `#0ea5e9` | all      |
| `logoUrl`      | `string` (URL) | –         | all      |
| `headerBanner` | `ReactNode`    | –         | web only |
| `mobilePresentation` | `'auto' \| 'sheet' \| 'card'` | `'auto'` | web |
| `mobileStepLabels` | `{ auth?: string; address?: string; payment?: string }` | Mobile/Address/Pay | web |


```tsx
<CheckoutWidget
  apiKey="..."
  brandColor="#7c3aed"
  logoUrl="https://yourbrand.com/logo.png"
  headerBanner={<p>🚚 Free shipping on orders over ₹499</p>}
  mobilePresentation="sheet"
  mobileStepLabels={{ auth: 'Mobile', address: 'Address', payment: 'Pay' }}
  onComplete={...}
/>
```

### Mobile-first storefront shells

Use `mobilePresentation="sheet"` when the widget is mounted inside a mobile drawer or storefront modal. It keeps the package focused on auth, saved addresses, DigiPin verification, and optional payment while your host app owns cart-specific UI.

```tsx
<CheckoutWidget
  apiKey={geocodingKey}
  merchantId={merchantId}
  supabaseFunctionBaseUrl={functionsUrl}
  enablePhoneAuth
  enablePayment={merchantPaymentMode === 'byo_razorpay'}
  paymentAmountPaise={cartTotalPaiseAfterDiscounts}
  codCartTotalPaise={cartTotalPaiseAfterDiscounts}
  mobilePresentation="sheet"
  mobileStepLabels={{ auth: 'Mobile', address: 'Address', payment: 'Pay' }}
  onCheckoutEvent={(event) => syncHostShellStep(event.step)}
  onComplete={(address) => saveDeliveryAddress(address)}
  onPaymentComplete={(result) => completeOrder(result)}
/>
```

The host app should keep order summary, product lines, coupons, cart state, platform APIs, and checkout-button interception outside this package. Pass only the final totals and callbacks into the widget.

### Analytics (client-side)


| Prop              | Type                             | Default | Platform |
| ----------------- | -------------------------------- | ------- | -------- |
| `onCheckoutEvent` | `(event: CheckoutEvent) => void` | –       | all      |


Called on every step transition. Use it to forward events to MoEngage, Clevertap, GTM, or your own backend — no server-side integration required.

```tsx
<CheckoutWidget
  apiKey="..."
  onCheckoutEvent={(event) => {
    // event.type — e.g. 'phone_verified', 'location_confirmed', 'address_completed'
    // event.step — widget step the user moved INTO
    // event.buyerId — internal buyer UUID (after phone verification)
    // event.digipin — DigiPin of the confirmed location
    // event.timestamp — ISO 8601 timestamp

    // MoEngage example:
    Moengage.track_event(event.type, {
      step: event.step,
      buyerId: event.buyerId,
      digipin: event.digipin,
    });

    // GTM example:
    window.dataLayer?.push({ event: event.type, ...event });
  }}
  onComplete={...}
/>
```

**All event types:**


| Event                    | When fired                              | Auth channel |
| ------------------------ | --------------------------------------- | ------------ |
| `phone_step_viewed`      | Phone entry screen rendered             | Phone        |
| `otp_sent`               | OTP dispatched via MSG91                | Phone        |
| `phone_verified`         | SMS OTP correct; session created        | Phone        |
| `email_step_viewed`      | Email input screen rendered             | Email        |
| `email_otp_sent`         | OTP dispatched via Resend               | Email        |
| `email_verified`         | Email OTP correct; session created      | Email        |
| `auth_skipped`           | Buyer tapped "Skip"                     | Both         |
| `saved_addresses_viewed` | Saved address list rendered             | –            |
| `saved_address_selected` | Buyer chose a saved address (skips map) | –            |
| `map_step_viewed`        | Map pin step rendered                   | –            |
| `location_confirmed`     | Buyer tapped "Confirm location"         | –            |
| `form_step_viewed`       | Address detail form rendered            | –            |
| `address_completed`      | Full address submitted                  | –            |
| `widget_reset`           | Buyer tapped "Change address"           | –            |


---

## `CompleteAddress` output

```ts
interface CompleteAddress {
  digipin: string;          // "39J-438-TJC7"
  lat: number;              // 28.61390
  lng: number;              // 77.20900

  // Auto-filled from QuantaRoute API
  state: string;            // "Delhi"
  district: string;         // "New Delhi"
  division: string;         // "New Delhi Central"
  locality: string;         // "Nirman Bhawan SO"
  pincode: string;          // "110011"
  delivery: string;         // "Delivery" | "Non Delivery"
  country: string;          // "India"

  // Manual entry by user
  flatNumber: string;       // "4B"
  floorNumber: string;      // "3rd"
  buildingName: string;     // "Sunshine Apartments"  (OSM pre-filled)
  streetName: string;       // "MG Road, Action Area" (OSM pre-filled)

  formattedAddress: string; // "4B, 3rd Floor, Sunshine Apartments, MG Road, ..."
  label?: 'home' | 'work' | 'other'; // chosen on address form; defaults to home
}
```

When `enablePhoneAuth` or `enableEmailAuth` is on, the widget persists the address via `createAddress()` using this label.

```ts
type AddressLabel = 'home' | 'work' | 'other';
```

---

## Advanced usage

### Use sub-components individually (web + native)

```tsx
import { MapPinSelector, AddressForm, getDigiPin, isWithinIndia } from '@quantaroute/checkout';

// Offline DigiPin — no API call, ~0.1 ms
const dp = getDigiPin(28.6139, 77.2090); // "39J-438-TJC7"
const ok = isWithinIndia(28.6139, 77.2090); // true

// Custom two-step flow
function MyCheckout() {
  const [loc, setLoc] = useState<{ lat: number; lng: number; digipin: string } | null>(null);

  return loc == null
    ? <MapPinSelector onLocationConfirm={(lat, lng, digipin) => setLoc({ lat, lng, digipin })} />
    : <AddressForm
        lat={loc.lat}
        lng={loc.lng}
        digipin={loc.digipin}
        apiKey="..."
        onAddressComplete={(addr) => console.log(addr)}
        onBack={() => setLoc(null)}
      />;
}
```

### Dark mode (web)

```tsx
<CheckoutWidget apiKey="..." theme="dark" onComplete={...} />
```

### Custom web theme via CSS variables

```css
.qr-checkout {
  --qr-primary:      #6366f1;
  --qr-primary-dark: #4f46e5;
  --qr-radius:       8px;
  --qr-font:         'Poppins', sans-serif;
}
```

### India boundary overlay (web only)

```tsx
<CheckoutWidget
  apiKey="..."
  indiaBoundaryUrl="/geojson/india.geojson"  {/* hosted in your public folder */}
  onComplete={...}
/>
```

---

## Supabase setup

> Only needed when using `**enablePhoneAuth**`. Skip this section for the basic map-only widget.

### 1 · Get your merchant credentials

Sign up at **[developers.quantaroute.com/onboarding/merchant](https://developers.quantaroute.com/onboarding/merchant)**. The wizard auto-provisions:

- `merchant_id` — your UUID in `checkout.merchants`
- Geocoding API key (for address search and reverse-geocoding)
- Integration snippet with all values filled in

### 2 · Deploy Edge Functions

The Edge Functions live in `supabase/functions/` inside this repo. Deploy all of them:

```bash
# Address CRUD + COD eligibility
supabase functions deploy checkout-addresses      --project-ref <your-project-ref> --no-verify-jwt

# Email OTP auth (Resend — no TRAI dependency, ship now)
supabase functions deploy checkout-email-otp      --project-ref <your-project-ref> --no-verify-jwt

# Phone OTP auth (MSG91 + TRAI DLT — deploy now, enable when DLT is ready)
supabase functions deploy checkout-otp            --project-ref <your-project-ref> --no-verify-jwt

# Session expiry — fires session.expired webhooks; runs every 10 min via pg_cron
supabase functions deploy checkout-session-expiry --project-ref <your-project-ref> --no-verify-jwt
```

### 3 · Set Edge Function secrets

In the [Supabase Dashboard](https://supabase.com/dashboard) → your project → Edge Functions → Secrets:

`**checkout-email-otp` (required for email auth):**


| Secret              | Value                                                  |
| ------------------- | ------------------------------------------------------ |
| `RESEND_API_KEY`    | `re_...` — from [resend.com](https://resend.com)       |
| `RESEND_FROM_EMAIL` | Verified sender address, e.g. `checkout@yourbrand.com` |


`**checkout-otp` (required for phone/SMS auth — set when TRAI DLT is approved):**


| Secret              | Value                                                                                                                   |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `MSG91_AUTHKEY`     | Your MSG91 AuthKey ([setup guide](./docs/MSG91_INDIA_OTP_SETUP.md)) — must match the Edge Function env var name exactly |
| `MSG91_TEMPLATE_ID` | Your approved DLT OTP template ID                                                                                       |


### 4 · Add your domain to the allowed list

In the developer portal at **[developers.quantaroute.com/dashboard/checkout](https://developers.quantaroute.com/dashboard/checkout)**, add your storefront's domain (e.g. `yourbrand.com`). The `checkout-email-otp` and `checkout-otp` Edge Functions enforce this list — requests from unlisted origins receive `403 Origin not allowed`.

### Minimal Next.js integration

#### Email OTP (available now — no TRAI paperwork)

```tsx
// app/checkout/page.tsx
'use client';
import dynamic from 'next/dynamic';

const CheckoutWidget = dynamic(
  () => import('@quantaroute/checkout').then((m) => m.CheckoutWidget),
  { ssr: false }
);

export default function CheckoutPage() {
  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      merchantId={process.env.NEXT_PUBLIC_MERCHANT_ID!}
      supabaseFunctionBaseUrl={process.env.NEXT_PUBLIC_SUPABASE_FUNCTION_URL!}
      enableEmailAuth={true}        {/* ← email OTP via Resend */}
      enableSearch={true}
      brandColor="#your-brand-hex"
      logoUrl="https://yourbrand.com/logo.png"
      onComplete={(address) => {
        // address.digipin, address.formattedAddress, address.pincode, ...
        // → pass to your backend / payment gateway
      }}
      onCheckoutEvent={(event) => {
        // email_step_viewed | email_otp_sent | email_verified | address_completed | ...
        console.log('[checkout]', event.type, event);
      }}
    />
  );
}
```

#### Phone OTP (enable when TRAI DLT is approved — same widget, one prop swap)

```tsx
<CheckoutWidget
  ...
  enablePhoneAuth={true}   {/* swap enableEmailAuth → enablePhoneAuth when SMS is ready */}
  enableEmailAuth={false}
/>
```

**Required env vars:**

```env
NEXT_PUBLIC_QUANTAROUTE_KEY=dp_...
NEXT_PUBLIC_MERCHANT_ID=your-uuid-from-developer-portal
NEXT_PUBLIC_SUPABASE_FUNCTION_URL=https://<project-ref>.supabase.co/functions/v1
```

---

## Architecture

```
@quantaroute/checkout/
├── src/
│   ├── components/
│   │   ├── CheckoutWidget.tsx           ← web  (MapLibre GL JS)
│   │   ├── CheckoutWidget.native.tsx    ← native (SafeAreaView)
│   │   ├── MapPinSelector.tsx           ← web  (MapLibre marker)
│   │   ├── MapPinSelector.native.tsx    ← native (expo-osm-sdk OSMView)
│   │   ├── AddressForm.tsx              ← web  (HTML form)
│   │   └── AddressForm.native.tsx       ← native (TextInput / Modal)
│   ├── core/
│   │   ├── digipin.ts     ← offline DigiPin algorithm (shared, no DOM)
│   │   ├── api.ts         ← QuantaRoute API client (shared, fetch)
│   │   └── types.ts       ← TypeScript types (shared)
│   ├── hooks/
│   │   ├── useGeolocation.ts            ← web  (navigator.geolocation)
│   │   ├── useGeolocation.native.ts     ← native (expo-location)
│   │   └── useDigiPin.ts                ← shared (pure math)
│   └── styles/
│       ├── checkout.css                 ← web styles
│       └── checkout.native.ts           ← native StyleSheet.create()
├── expo-plugin.js   ← Expo config plugin
├── babel.config.js  ← Metro/Babel config
└── dist/            ← web build output (Vite)
```

**Platform resolution:**

```
Metro (Expo app)          → "react-native" export → src/index.ts
                            → MapPinSelector.native.tsx (expo-osm-sdk)
                            → useGeolocation.native.ts (expo-location)

Vite / Webpack / Next.js  → "import" export → dist/lib/quantaroute-checkout.es.js
                            → MapPinSelector.tsx (MapLibre GL JS)
                            → useGeolocation.ts (navigator.geolocation)
```

---

## Map tile license

Uses **Carto Positron** vector tiles (Carto Voyager on native):

- Free for commercial use — attribution required
- Attribution: `© OpenStreetMap contributors © CARTO`
- No API key required for Carto basemaps

---

## DigiPin license

The offline DigiPin algorithm is the official India Post implementation:

- **Source:** [github.com/INDIAPOST-gov/digipin](https://github.com/INDIAPOST-gov/digipin)
- **Developed by:** India Post, IIT Hyderabad, ISRO NRSC
- **License:** Apache 2.0

---

## Documentation


| Doc                                                        | Description                                      |
| ---------------------------------------------------------- | ------------------------------------------------ |
| [Changelog](./CHANGELOG.md)                                | Version history and release notes                |
| [Checkout roadmap](./docs/CHECKOUT_ROADMAP.md)             | Feature phases and implementation status         |
| [Merchant onboarding](./docs/merchant-onboarding.md)       | Payment modes A/B/C, Razorpay setup (Mode B)     |
| [BYO payment gateway](./docs/byo-payment-gateway.md)       | Mode A — PayU/Cashfree/Paytm/Shopify integration   |
| [Hosted payments roadmap](./docs/hosted-payments-roadmap.md) | Mode C — QuantaRoute Payments (coming soon)    |
| [Razorpay E2E setup](./docs/razorpay-e2e-setup.md)         | Test-mode verification checklist (Mode B)        |
| [Customer migration playbook](./docs/customer-migration-playbook.md) | Merchant-facing import + lazy DigiPin story |
| [Portal payment wizard spec](./docs/portal-payment-wizard-spec.md) | Developer portal — `/checkout/payments` spec |
| [Portal migration wizard spec](./docs/portal-migration-wizard-spec.md) | Developer portal — `/checkout/migrate` spec |
| [MSG91 OTP setup](./docs/MSG91_INDIA_OTP_SETUP.md)         | India SMS / DLT configuration for phone OTP      |


---

## Security & integration

### What ships in the npm package

The published tarball contains **no secrets** — only client-side widget code:


| Included                                   | Excluded                                  |
| ------------------------------------------ | ----------------------------------------- |
| `dist/lib/*.js` (Terser-minified ES + UMD) | `supabase/`, Edge Functions, migrations   |
| `dist/lib/index.d.ts`                      | `demo/`, `docs/`, `openapi.yaml`, `.env*` |
| `dist/style.css`                           | `vite.config.ts`, dev tooling             |
| `src/` (required for React Native / Metro) | `node_modules/`                           |


- **Geocoding `apiKey`** (`dp_…`) is designed for the browser bundle — rate-limited, scoped to your merchant plan.
- **Admin API key** (`qca_…`) must **never** ship in frontend code. Use it server-side only (`x-api-key` on `checkout-admin`).
- OTP delivery (Resend) and session minting run on **QuantaRoute-hosted Edge Functions**, not inside the npm bundle.
- **Consistent email OTP:** the same 6-digit code is stored per `(email, merchant)` and reused for up to **10 successful logins** (not single-use). Repeat send requests skip Resend when the code is still active. This reduces email cost but is weaker than banking-grade OTP — document for your shoppers if required by policy.

Verify before every publish:

```bash
npm run type-check && npm run build:lib
npm pack --dry-run          # confirm 36 files, no .env / supabase / demo
rg -i 're_|sk_|service.role|password' dist/   # should return no matches
```

### Merchant integration checklist (email OTP on web)

Use this when embedding `@quantaroute/checkout` on a **hosted QuantaRoute** storefront (onboarded via [developers.quantaroute.com](https://developers.quantaroute.com)):

1. **Onboard** — complete [merchant onboarding](https://developers.quantaroute.com/onboarding/merchant); copy `merchantId`, geocoding `apiKey`, and `supabaseFunctionBaseUrl` from the wizard or [Checkout dashboard](https://developers.quantaroute.com/dashboard/checkout).
2. **Install** — `npm install @quantaroute/checkout maplibre-gl` (+ CSS imports).
3. **Env vars** — expose only public values (`NEXT_PUBLIC_QUANTAROUTE_KEY`, `NEXT_PUBLIC_MERCHANT_ID`, `NEXT_PUBLIC_SUPABASE_FUNCTION_URL`). Never commit `.env` to git.
4. **Allowed domains** — in the developer portal, add every origin that will host the widget (e.g. `yourbrand.com`, `www.yourbrand.com`, `*.yourbrand.com` for subdomains). Both `checkout-otp` and `checkout-email-otp` reject requests from unlisted origins.
5. **Enable auth** — set `enableEmailAuth={true}` with `merchantId` and `supabaseFunctionBaseUrl`. Auth is **opt-in** and **web-only** today (React Native widget does not include the email OTP steps).
6. **Optional branding** — `brandColor`, `logoUrl`; per-merchant email template columns (`email_from_name`, `email_logo_url`, `email_subject_template`) in the portal.
7. **Handle completion** — implement `onComplete` to pass the `CompleteAddress` to your cart / payment gateway. Built-in payment (`enablePayment`) and COD (`enableCod`) are separate opt-in props.

> **Resend is platform-managed for hosted merchants.** QuantaRouHMACte operates the shared `checkout-email-otp` Edge Function and Resend sender (`RESEND_API_KEY`, `RESEND_FROM_EMAIL`). Merchants do **not** need their own Resend account for the hosted MVP. See [Setting up Resend](#setting-up-resend-required-for-enableemailauth) only if you self-host the Edge Functions.

### Platform responsibilities (QuantaRoute)


| Item                                                                                                           | Owner                               |
| -------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| Deploy & operate `checkout-email-otp`, `checkout-addresses`, `checkout-session-expiry`                         | QuantaRoute                         |
| `RESEND_API_KEY`, `RESEND_FROM_EMAIL`, Supabase service role                                                   | QuantaRoute (Edge Function secrets) |
| `checkout.merchants` provisioning, `allowed_domains`, email template columns                                   | QuantaRoute portal + DB             |
| Buyer OTP consistent reuse (10 logins per code), fresh-code limit (3 / 24h), verify lockout (5 fails → 15 min) | Edge Function                       |
| Geocoding API key issuance & plan rate limits                                                                  | QuantaRoute                         |


### Maintainer publish checklist

When publishing a new version to npm (maintainers only):

1. Enable **npm 2FA** on the `@quantaroute` org account (publish + modify).
2. Run `npm run type-check && npm run build:lib && npm pack --dry-run` — confirm tarball contents.
3. Confirm no `.env`, `.pem`, or `supabase/` paths in the pack list.
4. Publish from a **clean git tag** with provenance: `npm publish --provenance --access public`.
5. Do **not** bump version in the same change unless releasing; current version is `1.4.5`.

---

## Development

```bash
git clone https://github.com/quantaroute/checkout.git
cd quantaroute-checkout
npm install

# Web dev server
npm run dev              # http://localhost:5173

# Type checks
npm run type-check       # web (Vite tsconfig)
npm run type-check:native # native (RN tsconfig)

# Build
npm run build:lib        # library → dist/lib/
npm run build            # library + demo
```

**Native demo:**

```bash
cd demo-native
npm install
npx expo run:ios         # requires Xcode
npx expo run:android     # requires Android Studio
```

---

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for version history. Current release: **v1.4.5**.

---

**Made with ❤️ in India · Powered by [QuantaRoute](https://quantaroute.com)**