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

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
| 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
Never hard-code or commit API keys to git.
Web (React / Next.js / Vite / Nuxt)
Install
npm install @quantaroute/checkout maplibre-gl
Import CSS
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
Use
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)
'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:
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
Next.js (Pages Router)
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
export default defineNuxtConfig({
css: ['maplibre-gl/dist/maplibre-gl.css', '@quantaroute/checkout/style.css'],
});
<!-- 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
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)
<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.
This does NOT work in Expo Go — expo-osm-sdk uses native modules.
1 · Install
npm install @quantaroute/checkout expo-osm-sdk expo-location
2 · Add the config plugin to app.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:
| 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
npx expo run:ios
npx expo run:android
4 · Use — identical import as web
import { CheckoutWidget } from '@quantaroute/checkout';
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/:
cd demo-native
npm install
npx expo run:ios
npx expo run:android
Props
Core props
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
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)
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:
<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)
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.
<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) => {
console.log('[checkout]', event.type, event);
}}
/>
Client-side events emitted during email auth:
email_step_viewed | Email input screen rendered |
email_otp_sent | OTP dispatched via Resend |
email_verified | OTP correct → session created |
Setting up Resend (required for enableEmailAuth)
Hosted merchants (recommended): If you onboarded at 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.
-
Create a Resend account at resend.com — free tier covers ~3 000 emails / month.
-
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)
-
Get an API key — Resend → API Keys → Create. Restrict it to Send access only.
-
Set secrets on the checkout-email-otp Edge Function:
In Supabase Dashboard → your project → Edge Functions → Secrets:
RESEND_API_KEY | re_... from Resend |
RESEND_FROM_EMAIL | A verified sender address, e.g. checkout@yourbrand.com |
-
Deploy the Edge Function (if not already done):
supabase functions deploy checkout-email-otp --project-ref <your-project-ref> --no-verify-jwt
Required env vars (frontend):
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.
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:
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
brandColor | string (hex) | #0ea5e9 | all |
logoUrl | string (URL) | – | all |
headerBanner | ReactNode | – | web only |
<CheckoutWidget
apiKey="..."
brandColor="#7c3aed"
logoUrl="https://yourbrand.com/logo.png"
headerBanner={<p>🚚 Free shipping on orders over ₹499</p>}
onComplete={...}
/>
Analytics (client-side)
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.
<CheckoutWidget
apiKey="..."
onCheckoutEvent={(event) => {
Moengage.track_event(event.type, {
step: event.step,
buyerId: event.buyerId,
digipin: event.digipin,
});
window.dataLayer?.push({ event: event.type, ...event });
}}
onComplete={...}
/>
All event types:
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
interface CompleteAddress {
digipin: string;
lat: number;
lng: number;
state: string;
district: string;
division: string;
locality: string;
pincode: string;
delivery: string;
country: string;
flatNumber: string;
floorNumber: string;
buildingName: string;
streetName: string;
formattedAddress: string;
label?: 'home' | 'work' | 'other';
}
When enablePhoneAuth or enableEmailAuth is on, the widget persists the address via createAddress() using this label.
type AddressLabel = 'home' | 'work' | 'other';
Advanced usage
Use sub-components individually (web + native)
import { MapPinSelector, AddressForm, getDigiPin, isWithinIndia } from '@quantaroute/checkout';
const dp = getDigiPin(28.6139, 77.2090);
const ok = isWithinIndia(28.6139, 77.2090);
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)
<CheckoutWidget apiKey="..." theme="dark" onComplete={...} />
Custom web theme via CSS variables
.qr-checkout {
--qr-primary: #6366f1;
--qr-primary-dark: #4f46e5;
--qr-radius: 8px;
--qr-font: 'Poppins', sans-serif;
}
India boundary overlay (web only)
<CheckoutWidget
apiKey="..."
indiaBoundaryUrl="/geojson/india.geojson" {}
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. 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:
supabase functions deploy checkout-addresses --project-ref <your-project-ref> --no-verify-jwt
supabase functions deploy checkout-email-otp --project-ref <your-project-ref> --no-verify-jwt
supabase functions deploy checkout-otp --project-ref <your-project-ref> --no-verify-jwt
supabase functions deploy checkout-session-expiry --project-ref <your-project-ref> --no-verify-jwt
3 · Set Edge Function secrets
In the Supabase Dashboard → your project → Edge Functions → Secrets:
checkout-email-otp (required for email auth):
RESEND_API_KEY | re_... — from 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):
MSG91_AUTHKEY | Your MSG91 AuthKey (setup guide) — 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, 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)
'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)
<CheckoutWidget
...
enablePhoneAuth={true} {}
enableEmailAuth={false}
/>
Required env vars:
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:
Documentation
Security & integration
What ships in the npm package
The published tarball contains no secrets — only client-side widget code:
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:
npm run type-check && npm run build:lib
npm pack --dry-run
rg -i 're_|sk_|service.role|password' dist/
Merchant integration checklist (email OTP on web)
Use this when embedding @quantaroute/checkout on a hosted QuantaRoute storefront (onboarded via developers.quantaroute.com):
- Onboard — complete merchant onboarding; copy
merchantId, geocoding apiKey, and supabaseFunctionBaseUrl from the wizard or Checkout dashboard.
- Install —
npm install @quantaroute/checkout maplibre-gl (+ CSS imports).
- Env vars — expose only public values (
NEXT_PUBLIC_QUANTAROUTE_KEY, NEXT_PUBLIC_MERCHANT_ID, NEXT_PUBLIC_SUPABASE_FUNCTION_URL). Never commit .env to git.
- 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.
- 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).
- Optional branding —
brandColor, logoUrl; per-merchant email template columns (email_from_name, email_logo_url, email_subject_template) in the portal.
- 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. QuantaRoute 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 only if you self-host the Edge Functions.
Platform responsibilities (QuantaRoute)
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):
- Enable npm 2FA on the
@quantaroute org account (publish + modify).
- Run
npm run type-check && npm run build:lib && npm pack --dry-run — confirm tarball contents.
- Confirm no
.env, .pem, or supabase/ paths in the pack list.
- Publish from a clean git tag with provenance:
npm publish --provenance --access public.
- Do not bump version in the same change unless releasing; current version is
1.4.4.
Development
git clone https://github.com/quantaroute/checkout.git
cd quantaroute-checkout
npm install
npm run dev
npm run type-check
npm run type-check:native
npm run build:lib
npm run build
Native demo:
cd demo-native
npm install
npx expo run:ios
npx expo run:android
Changelog
See CHANGELOG.md for version history. Current release: v1.4.4.
Made with ❤️ in India · Powered by QuantaRoute