npm package: @quantaroute/checkout
Description: Embeddable DigiPin-powered smart address checkout widget — React (web), React Native & Expo (iOS/Android)
Author: QuantaRoute
Latest version: 1.3.2

# @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://img.shields.io/badge/Made%20in-India%20🇮🇳-orange)](https://quantaroute.com)
[![DigiPin](https://img.shields.io/badge/DigiPin-India%20Post-blue)](https://www.indiapost.gov.in)
[![Map](https://img.shields.io/badge/Map-Carto%20Positron%20(free)-green)](https://carto.com)
[![License](https://img.shields.io/badge/License-MIT-lightgrey)](./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

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

---

## `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, ..."
}
```

---

## 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={...}
/>
```

---

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

---

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

### v1.2.0

- **iOS & Android support** via `expo-osm-sdk` (MapLibre GL Native)
- `MapPinSelector.native.tsx` — draggable OSMView pin
- `AddressForm.native.tsx` — `TextInput` / `ScrollView` / `Modal` locality picker
- `CheckoutWidget.native.tsx` — `SafeAreaView` layout with identical flow logic
- `useGeolocation.native.ts` — GPS via `expo-location`
- `checkout.native.ts` — complete `StyleSheet` design system (light + dark)
- `expo-plugin.js` — one-line `app.json` setup for iOS/Android
- `babel.config.js` — Metro transpilation config
- `mapHeight` prop now accepts `number` (native) or `string` (web, e.g. `'380px'`)
- All native peer dependencies marked optional (zero impact on web bundles)

### v1.1.x

- DigiPin badge overlay on map
- Locality alternatives dropdown
- OSM address pre-fill (building name, road, suburb)
- India boundary GeoJSON overlay

### v1.0.0

- Initial release: MapLibre GL + Carto Positron
- Offline DigiPin generation
- QuantaRoute API integration
- Mobile-first responsive design
- Dark mode, reduced-motion, keyboard accessible

---

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