@quantaroute/checkout 1.2.0 โ†’ 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![Made in India](https://img.shields.io/badge/Made%20in-India%20๐Ÿ‡ฎ๐Ÿ‡ณ-orange)](https://quantaroute.com)
8
8
  [![DigiPin](https://img.shields.io/badge/DigiPin-India%20Post-blue)](https://www.indiapost.gov.in)
9
- [![Map](https://img.shields.io/badge/Map-Carto%20Positron%20(free)-green)](https://carto.com)
9
+ [![Map](https://img.shields.io/badge/Map-MapLibre%20%2B%20OpenFreeMap%20%2F%20Carto-green)](https://openfreemap.org)
10
10
  [![License](https://img.shields.io/badge/License-MIT-lightgrey)](./LICENSE)
11
11
 
12
12
  ---
@@ -32,7 +32,7 @@ Step 1 โ€“ Map Pin Step 2 โ€“ Auto-fill + Details
32
32
  **Key features:**
33
33
 
34
34
  - DigiPin shown **offline** in real-time as the user drags the pin (~4 m ร— 4 m precision)
35
- - **No Google Maps.** Free Carto Positron vector basemap
35
+ - **No Google Maps.** Free vector basemap โ€” choose from Carto Positron or OpenFreeMap styles (no API key for either)
36
36
  - Auto-fills State, District, Locality, Pincode, Delivery status from QuantaRoute API
37
37
  - Manual fields: Flat no., Floor, Building (OSM pre-filled), Street/Area (OSM pre-filled)
38
38
  - Mobile-first (full-screen on phones, card on desktop/tablet)
@@ -297,6 +297,7 @@ npx expo run:android
297
297
  | `theme` | `'light' \| 'dark'` | `'light'` | all |
298
298
  | `mapHeight` | `string \| number` | `'380px'` / `380` | all |
299
299
  | `title` | `string` | `'Add Delivery Address'` | all |
300
+ | `mapStyle` | `BuiltInMapStyle \| string` | `'carto-positron'` | all |
300
301
  | `className` | `string` | โ€“ | web only |
301
302
  | `style` | `CSSProperties \| StyleProp<ViewStyle>` | โ€“ | all |
302
303
  | `indiaBoundaryUrl` | `string` | โ€“ | web only |
@@ -377,6 +378,25 @@ function MyCheckout() {
377
378
  }
378
379
  ```
379
380
 
381
+ ### Choose a basemap style
382
+
383
+ Pass a built-in preset name or any MapLibre-compatible style URL:
384
+
385
+ ```tsx
386
+ // Built-in presets (no API key required for any of them)
387
+ <CheckoutWidget apiKey="..." mapStyle="carto-positron" onComplete={...} /> {/* default */}
388
+ <CheckoutWidget apiKey="..." mapStyle="openfreemap-liberty" onComplete={...} /> {/* colorful OSM */}
389
+ <CheckoutWidget apiKey="..." mapStyle="openfreemap-positron" onComplete={...} /> {/* clean light */}
390
+ <CheckoutWidget apiKey="..." mapStyle="openfreemap-bright" onComplete={...} /> {/* vibrant */}
391
+
392
+ // Custom style URL (your own MapLibre server)
393
+ <CheckoutWidget apiKey="..." mapStyle="https://my-tiles.example.com/style.json" onComplete={...} />
394
+ ```
395
+
396
+ Attribution is applied automatically for each built-in preset.
397
+
398
+ > OpenFreeMap is a free, open-source tile hosting service. If you use the `openfreemap-*` presets in production, consider [sponsoring the project](https://github.com/sponsors/hyperknot) to keep the public instance running.
399
+
380
400
  ### India boundary overlay (web only)
381
401
 
382
402
  ```tsx
@@ -404,6 +424,7 @@ function MyCheckout() {
404
424
  โ”‚ โ”œโ”€โ”€ core/
405
425
  โ”‚ โ”‚ โ”œโ”€โ”€ digipin.ts โ† offline DigiPin algorithm (shared, no DOM)
406
426
  โ”‚ โ”‚ โ”œโ”€โ”€ api.ts โ† QuantaRoute API client (shared, fetch)
427
+ โ”‚ โ”‚ โ”œโ”€โ”€ mapStyles.ts โ† basemap preset registry + resolver (shared)
407
428
  โ”‚ โ”‚ โ””โ”€โ”€ types.ts โ† TypeScript types (shared)
408
429
  โ”‚ โ”œโ”€โ”€ hooks/
409
430
  โ”‚ โ”‚ โ”œโ”€โ”€ useGeolocation.ts โ† web (navigator.geolocation)
@@ -433,11 +454,16 @@ Vite / Webpack / Next.js โ†’ "import" export โ†’ dist/lib/quantaroute-checkout.
433
454
 
434
455
  ## Map tile license
435
456
 
436
- Uses **Carto Positron** vector tiles (Carto Voyager on native):
457
+ | Preset | Provider | Attribution | API key |
458
+ |---|---|---|---|
459
+ | `carto-positron` (default) | [Carto](https://carto.com/attributions) | `ยฉ OpenStreetMap contributors ยฉ CARTO` | None |
460
+ | `openfreemap-liberty` | [OpenFreeMap](https://openfreemap.org) | `ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap` | None |
461
+ | `openfreemap-positron` | [OpenFreeMap](https://openfreemap.org) | `ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap` | None |
462
+ | `openfreemap-bright` | [OpenFreeMap](https://openfreemap.org) | `ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap` | None |
463
+
464
+ Attribution is injected automatically for all built-in presets. When using a custom style URL you are responsible for correct attribution per your tile provider's terms.
437
465
 
438
- - Free for commercial use โ€” attribution required
439
- - Attribution: `ยฉ OpenStreetMap contributors ยฉ CARTO`
440
- - No API key required for Carto basemaps
466
+ OpenFreeMap is fully open-source (MIT). If you use `openfreemap-*` presets in production, please consider [sponsoring their public instance](https://github.com/sponsors/hyperknot).
441
467
 
442
468
  ---
443
469
 
@@ -483,6 +509,17 @@ npx expo run:android # requires Android Studio
483
509
 
484
510
  ## Changelog
485
511
 
512
+ ### v1.3.0
513
+
514
+ - **`mapStyle` prop** โ€” choose a basemap preset or pass any MapLibre-compatible style URL
515
+ - `'carto-positron'` (default, unchanged โ€” fully backward-compatible)
516
+ - `'openfreemap-liberty'` โ€” colorful OSM-flavored style via [OpenFreeMap](https://openfreemap.org)
517
+ - `'openfreemap-positron'` โ€” clean light style via OpenFreeMap
518
+ - `'openfreemap-bright'` โ€” vibrant high-contrast style via OpenFreeMap
519
+ - Custom URL string โ€” pass any MapLibre style JSON endpoint
520
+ - Attribution text is resolved automatically per provider (no manual setup needed)
521
+ - New `src/core/mapStyles.ts` โ€” shared preset registry used by both web and native components
522
+
486
523
  ### v1.2.0
487
524
 
488
525
  - **iOS & Android support** via `expo-osm-sdk` (MapLibre GL Native)
@@ -50,6 +50,23 @@ export declare interface AdministrativeInfo {
50
50
 
51
51
  declare type AnyStyle = any;
52
52
 
53
+ /**
54
+ * Built-in basemap style presets.
55
+ *
56
+ * | Preset | Provider | Character |
57
+ * |--------------------------|---------------|----------------------------------|
58
+ * | `'carto-positron'` | Carto | Clean, minimal, light (default) |
59
+ * | `'openfreemap-liberty'` | OpenFreeMap | Colorful OSM-flavored |
60
+ * | `'openfreemap-positron'` | OpenFreeMap | Clean light, open-source |
61
+ * | `'openfreemap-bright'` | OpenFreeMap | Vibrant, high contrast |
62
+ *
63
+ * You can also pass any MapLibre-compatible style URL string directly.
64
+ *
65
+ * OpenFreeMap is free and open-source with no API key or rate limits.
66
+ * See https://openfreemap.org
67
+ */
68
+ declare type BuiltInMapStyle = 'carto-positron' | 'openfreemap-liberty' | 'openfreemap-positron' | 'openfreemap-bright';
69
+
53
70
  /**
54
71
  * QuantaRoute Checkout Widget
55
72
  *
@@ -90,6 +107,13 @@ export declare interface CheckoutWidgetProps {
90
107
  mapHeight?: string | number;
91
108
  /** Widget header title. Defaults to 'Add Delivery Address'. */
92
109
  title?: string;
110
+ /**
111
+ * Basemap style. Pass a preset name or any MapLibre-compatible style URL.
112
+ * Defaults to `'carto-positron'`.
113
+ * @example mapStyle="openfreemap-liberty"
114
+ * @example mapStyle="https://my-server.com/style.json"
115
+ */
116
+ mapStyle?: BuiltInMapStyle | string;
93
117
  /**
94
118
  * URL to an India boundary GeoJSON file (FeatureCollection).
95
119
  * When provided, a thin grey outline is drawn on the map for legal compliance.
@@ -215,6 +239,13 @@ export declare interface MapPinSelectorProps {
215
239
  /** Map height. String on web ('380px'), number on native (380). */
216
240
  mapHeight?: string | number;
217
241
  theme?: 'light' | 'dark';
242
+ /**
243
+ * Basemap style. Pass a preset name or any MapLibre-compatible style URL.
244
+ * Defaults to `'carto-positron'`.
245
+ * @example mapStyle="openfreemap-liberty"
246
+ * @example mapStyle="https://my-server.com/style.json"
247
+ */
248
+ mapStyle?: BuiltInMapStyle | string;
218
249
  /**
219
250
  * URL to an India boundary GeoJSON file (FeatureCollection).
220
251
  * When provided, a thin grey outline of India's border is drawn on the map
@@ -154,11 +154,49 @@ function useGeolocation() {
154
154
  }, []);
155
155
  return { ...state, locate, clearError };
156
156
  }
157
+ const STYLE_REGISTRY = {
158
+ "carto-positron": {
159
+ url: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
160
+ attributionHtml: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>',
161
+ attributionPlain: "ยฉ OpenStreetMap contributors ยฉ CARTO"
162
+ },
163
+ "openfreemap-liberty": {
164
+ url: "https://tiles.openfreemap.org/styles/liberty",
165
+ attributionHtml: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
166
+ attributionPlain: "ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"
167
+ },
168
+ "openfreemap-positron": {
169
+ url: "https://tiles.openfreemap.org/styles/positron",
170
+ attributionHtml: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
171
+ attributionPlain: "ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"
172
+ },
173
+ "openfreemap-bright": {
174
+ url: "https://tiles.openfreemap.org/styles/bright",
175
+ attributionHtml: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
176
+ attributionPlain: "ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"
177
+ }
178
+ };
179
+ const DEFAULT_STYLE = "carto-positron";
180
+ function isBuiltIn(value) {
181
+ return value in STYLE_REGISTRY;
182
+ }
183
+ function resolveMapStyle(mapStyle) {
184
+ if (!mapStyle || mapStyle === DEFAULT_STYLE) {
185
+ return STYLE_REGISTRY[DEFAULT_STYLE];
186
+ }
187
+ if (isBuiltIn(mapStyle)) {
188
+ return STYLE_REGISTRY[mapStyle];
189
+ }
190
+ return {
191
+ url: mapStyle,
192
+ attributionHtml: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors',
193
+ attributionPlain: "ยฉ OpenStreetMap contributors"
194
+ };
195
+ }
157
196
  const INDIA_CENTER = { lat: 13.00427, lng: 77.589291 };
158
197
  const MIN_ZOOM = 6;
159
198
  const OVERVIEW_ZOOM = 9;
160
199
  const STREET_ZOOM = 18;
161
- const CARTO_STYLE_URL = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json";
162
200
  const PIN_W = 40;
163
201
  const PIN_H = 52;
164
202
  function computeDigiPinSafe(la, lo) {
@@ -225,6 +263,7 @@ const MapPinSelector = ({
225
263
  mapHeight = "380px",
226
264
  theme: _theme = "light",
227
265
  // kept for future dark-mode pin tint; unused for now
266
+ mapStyle,
228
267
  indiaBoundaryUrl
229
268
  }) => {
230
269
  const containerRef = useRef(null);
@@ -247,9 +286,10 @@ const MapPinSelector = ({
247
286
  if (!containerRef.current) return;
248
287
  const boundaryPromise = indiaBoundaryUrl ? fetch(indiaBoundaryUrl).then((r) => r.ok ? r.json() : null).catch(() => null) : Promise.resolve(null);
249
288
  const initZoom = hasDefault ? STREET_ZOOM : OVERVIEW_ZOOM;
289
+ const { url: styleUrl, attributionHtml } = resolveMapStyle(mapStyle);
250
290
  const map = new maplibregl.Map({
251
291
  container: containerRef.current,
252
- style: CARTO_STYLE_URL,
292
+ style: styleUrl,
253
293
  center: [initLng, initLat],
254
294
  zoom: initZoom,
255
295
  minZoom: MIN_ZOOM,
@@ -261,7 +301,7 @@ const MapPinSelector = ({
261
301
  map.addControl(
262
302
  new maplibregl.AttributionControl({
263
303
  compact: true,
264
- customAttribution: 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>'
304
+ customAttribution: attributionHtml
265
305
  }),
266
306
  "bottom-right"
267
307
  );
@@ -451,6 +491,14 @@ const MapPinSelector = ({
451
491
  ] });
452
492
  };
453
493
  const DEFAULT_BASE_URL = "https://api.quantaroute.com";
494
+ function makeTimeoutSignal(ms) {
495
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
496
+ return AbortSignal.timeout(ms);
497
+ }
498
+ const ctrl = new AbortController();
499
+ setTimeout(() => ctrl.abort(), ms);
500
+ return ctrl.signal;
501
+ }
454
502
  async function lookupLocation(lat, lng, apiKey, baseUrl = DEFAULT_BASE_URL) {
455
503
  const url = `${baseUrl.replace(/\/$/, "")}/v1/location/lookup`;
456
504
  let res;
@@ -462,11 +510,10 @@ async function lookupLocation(lat, lng, apiKey, baseUrl = DEFAULT_BASE_URL) {
462
510
  "x-api-key": apiKey
463
511
  },
464
512
  body: JSON.stringify({ latitude: lat, longitude: lng }),
465
- signal: AbortSignal.timeout(15e3)
466
- // 15s timeout
513
+ signal: makeTimeoutSignal(15e3)
467
514
  });
468
515
  } catch (err) {
469
- if (err instanceof Error && err.name === "TimeoutError") {
516
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
470
517
  throw new Error("Request timed out. Check your internet connection.");
471
518
  }
472
519
  throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
@@ -502,11 +549,10 @@ async function reverseGeocode(digipin, apiKey, baseUrl = DEFAULT_BASE_URL) {
502
549
  "x-api-key": apiKey
503
550
  },
504
551
  body: JSON.stringify({ digipin }),
505
- signal: AbortSignal.timeout(15e3)
506
- // 15s timeout
552
+ signal: makeTimeoutSignal(15e3)
507
553
  });
508
554
  } catch (err) {
509
- if (err instanceof Error && err.name === "TimeoutError") {
555
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
510
556
  throw new Error("Request timed out. Check your internet connection.");
511
557
  }
512
558
  throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
@@ -940,6 +986,7 @@ const CheckoutWidget = ({
940
986
  style,
941
987
  mapHeight = "380px",
942
988
  title = "Add Delivery Address",
989
+ mapStyle,
943
990
  indiaBoundaryUrl
944
991
  }) => {
945
992
  const [step, setStep] = useState("map");
@@ -1005,6 +1052,7 @@ const CheckoutWidget = ({
1005
1052
  defaultLng,
1006
1053
  mapHeight,
1007
1054
  theme,
1055
+ mapStyle,
1008
1056
  indiaBoundaryUrl
1009
1057
  }
1010
1058
  ),
@@ -1,4 +1,4 @@
1
- !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("react/jsx-runtime"),require("react"),require("maplibre-gl")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","react","maplibre-gl"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).QuantaRouteCheckout={},e.ReactJSXRuntime,e.React,e.maplibregl)}(this,function(e,r,a,n){"use strict"
1
+ !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("react/jsx-runtime"),require("react"),require("maplibre-gl")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","react","maplibre-gl"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).QuantaRouteCheckout={},e.ReactJSXRuntime,e.React,e.maplibregl)}(this,function(e,r,t,n){"use strict"
2
2
 
3
3
  ;/*!
4
4
  * @quantaroute/checkout v1.2.0
@@ -19,76 +19,79 @@
19
19
  * expo >= 49.0.0
20
20
  * expo-osm-sdk >= 2.0.0
21
21
  * expo-location >= 17.0.0
22
- */const t=[["F","C","9","8"],["J","3","2","7"],["K","4","5","6"],["L","M","P","T"]],i={minLat:2.5,maxLat:38.5,minLon:63.5,maxLon:99.5}
22
+ */const a=[["F","C","9","8"],["J","3","2","7"],["K","4","5","6"],["L","M","P","T"]],i={minLat:2.5,maxLat:38.5,minLon:63.5,maxLon:99.5}
23
23
  function o(e,r){if(e<i.minLat||e>i.maxLat)throw new Error(`Latitude ${e} out of range. Must be between ${i.minLat} and ${i.maxLat}`)
24
24
  if(r<i.minLon||r>i.maxLon)throw new Error(`Longitude ${r} out of range. Must be between ${i.minLon} and ${i.maxLon}`)
25
- let a=i.minLat,n=i.maxLat,o=i.minLon,l=i.maxLon,s=""
26
- for(let i=1;i<=10;i++){const d=(n-a)/4,c=(l-o)/4
27
- let u=3-Math.floor((e-a)/d),h=Math.floor((r-o)/c)
28
- u=Math.max(0,Math.min(u,3)),h=Math.max(0,Math.min(h,3)),s+=t[u][h],3!==i&&6!==i||(s+="-"),n=a+d*(4-u),a+=d*(3-u),o+=c*h,l=o+c}return s}function l(e){const r=e.replace(/-/g,"")
25
+ let t=i.minLat,n=i.maxLat,o=i.minLon,l=i.maxLon,s=""
26
+ for(let i=1;i<=10;i++){const c=(n-t)/4,d=(l-o)/4
27
+ let p=3-Math.floor((e-t)/c),u=Math.floor((r-o)/d)
28
+ p=Math.max(0,Math.min(p,3)),u=Math.max(0,Math.min(u,3)),s+=a[p][u],3!==i&&6!==i||(s+="-"),n=t+c*(4-p),t+=c*(3-p),o+=d*u,l=o+d}return s}function l(e){const r=e.replace(/-/g,"")
29
29
  if(10!==r.length)throw new Error("Invalid DIGIPIN: must be 10 characters (excluding dashes)")
30
- let a=i.minLat,n=i.maxLat,o=i.minLon,l=i.maxLon
30
+ let t=i.minLat,n=i.maxLat,o=i.minLon,l=i.maxLon
31
31
  for(let i=0;i<10;i++){const e=r[i]
32
- let s=!1,d=-1,c=-1
33
- for(let r=0;r<4;r++){for(let a=0;a<4;a++)if(t[r][a]===e){d=r,c=a,s=!0
32
+ let s=!1,c=-1,d=-1
33
+ for(let r=0;r<4;r++){for(let t=0;t<4;t++)if(a[r][t]===e){c=r,d=t,s=!0
34
34
  break}if(s)break}if(!s)throw new Error(`Invalid character in DIGIPIN: '${e}'`)
35
- const u=(n-a)/4,h=(l-o)/4,p=o+h*(c+1)
36
- a=n-u*(d+1),n-=u*d,o+=h*c,l=p}return{latitude:((a+n)/2).toFixed(6),longitude:((o+l)/2).toFixed(6)}}function s(e,r){return e>=i.minLat&&e<=i.maxLat&&r>=i.minLon&&r<=i.maxLon}function d(){const[e,r]=a.useState({loading:!1,error:null})
37
- return{...e,locate:a.useCallback(e=>{navigator.geolocation?(r({loading:!0,error:null}),navigator.geolocation.getCurrentPosition(({coords:a})=>{r({loading:!1,error:null}),e(a.latitude,a.longitude)},e=>{let a="Unable to get location."
38
- e.code===e.PERMISSION_DENIED?a="Location permission denied. Please enable it in browser settings.":e.code===e.POSITION_UNAVAILABLE?a="Location unavailable. Try again or place the pin manually.":e.code===e.TIMEOUT&&(a="Location request timed out. Try again."),r({loading:!1,error:a})},{enableHighAccuracy:!0,timeout:12e3,maximumAge:0})):r({loading:!1,error:"Geolocation is not supported by your browser."})},[]),clearError:a.useCallback(()=>{r(e=>({...e,error:null}))},[])}}function c(e,r){if(!s(e,r))return null
39
- try{return o(e,r)}catch{return null}}const u=({onLocationConfirm:e,defaultLat:t,defaultLng:i,mapHeight:o="380px",theme:l="light",indiaBoundaryUrl:s})=>{const u=a.useRef(null),h=a.useRef(null),p=a.useRef(null),m=t??13.00427,g=i??77.589291,f=void 0!==t&&void 0!==i,[q,b]=a.useState(m),[y,N]=a.useState(g),[v,_]=a.useState(()=>c(m,g)),[w,k]=a.useState(!1),{loading:x,error:L,locate:C,clearError:E}=d(),S=a.useCallback((e,r)=>{_(c(e,r))},[])
40
- a.useEffect(()=>{if(!u.current)return
41
- const e=s?fetch(s).then(e=>e.ok?e.json():null).catch(()=>null):Promise.resolve(null),r=f?18:9,a=new n.Map({container:u.current,style:"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",center:[g,m],zoom:r,minZoom:6,attributionControl:!1,touchZoomRotate:!0})
42
- return a.addControl(new n.AttributionControl({compact:!0,customAttribution:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>'}),"bottom-right"),a.addControl(new n.NavigationControl({showCompass:!1}),"top-right"),a.on("load",()=>{k(!0),e.then(e=>{if(e&&a.isStyleLoaded())try{a.addSource("india-boundary",{type:"geojson",data:e}),a.addLayer({id:"india-boundary-line",type:"line",source:"india-boundary",paint:{"line-color":"#94a3b8","line-width":1.5,"line-opacity":.65}})}catch{}})
35
+ const p=(n-t)/4,u=(l-o)/4,h=o+u*(d+1)
36
+ t=n-p*(c+1),n-=p*c,o+=u*d,l=h}return{latitude:((t+n)/2).toFixed(6),longitude:((o+l)/2).toFixed(6)}}function s(e,r){return e>=i.minLat&&e<=i.maxLat&&r>=i.minLon&&r<=i.maxLon}function c(){const[e,r]=t.useState({loading:!1,error:null})
37
+ return{...e,locate:t.useCallback(e=>{navigator.geolocation?(r({loading:!0,error:null}),navigator.geolocation.getCurrentPosition(({coords:t})=>{r({loading:!1,error:null}),e(t.latitude,t.longitude)},e=>{let t="Unable to get location."
38
+ e.code===e.PERMISSION_DENIED?t="Location permission denied. Please enable it in browser settings.":e.code===e.POSITION_UNAVAILABLE?t="Location unavailable. Try again or place the pin manually.":e.code===e.TIMEOUT&&(t="Location request timed out. Try again."),r({loading:!1,error:t})},{enableHighAccuracy:!0,timeout:12e3,maximumAge:0})):r({loading:!1,error:"Geolocation is not supported by your browser."})},[]),clearError:t.useCallback(()=>{r(e=>({...e,error:null}))},[])}}const d={"carto-positron":{url:"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",attributionHtml:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>',attributionPlain:"ยฉ OpenStreetMap contributors ยฉ CARTO"},"openfreemap-liberty":{url:"https://tiles.openfreemap.org/styles/liberty",attributionHtml:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',attributionPlain:"ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"},"openfreemap-positron":{url:"https://tiles.openfreemap.org/styles/positron",attributionHtml:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',attributionPlain:"ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"},"openfreemap-bright":{url:"https://tiles.openfreemap.org/styles/bright",attributionHtml:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',attributionPlain:"ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap"}},p="carto-positron"
39
+ function u(e,r){if(!s(e,r))return null
40
+ try{return o(e,r)}catch{return null}}const h=({onLocationConfirm:e,defaultLat:a,defaultLng:i,mapHeight:o="380px",theme:l="light",mapStyle:s,indiaBoundaryUrl:h})=>{const m=t.useRef(null),g=t.useRef(null),f=t.useRef(null),b=a??13.00427,q=i??77.589291,y=void 0!==a&&void 0!==i,[_,N]=t.useState(b),[v,w]=t.useState(q),[k,x]=t.useState(()=>u(b,q)),[S,L]=t.useState(!1),{loading:M,error:C,locate:E,clearError:A}=c(),T=t.useCallback((e,r)=>{x(u(e,r))},[])
41
+ t.useEffect(()=>{if(!m.current)return
42
+ const e=h?fetch(h).then(e=>e.ok?e.json():null).catch(()=>null):Promise.resolve(null),r=y?18:9,{url:t,attributionHtml:a}=function(e){return e&&e!==p?e in d?d[e]:{url:e,attributionHtml:'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors',attributionPlain:"ยฉ OpenStreetMap contributors"}:d[p]}(s),i=new n.Map({container:m.current,style:t,center:[q,b],zoom:r,minZoom:6,attributionControl:!1,touchZoomRotate:!0})
43
+ return i.addControl(new n.AttributionControl({compact:!0,customAttribution:a}),"bottom-right"),i.addControl(new n.NavigationControl({showCompass:!1}),"top-right"),i.on("load",()=>{L(!0),e.then(e=>{if(e&&i.isStyleLoaded())try{i.addSource("india-boundary",{type:"geojson",data:e}),i.addLayer({id:"india-boundary-line",type:"line",source:"india-boundary",paint:{"line-color":"#94a3b8","line-width":1.5,"line-opacity":.65}})}catch{}})
43
44
  const r=function(){const e=document.createElement("div")
44
- return e.className="qr-pin",e.setAttribute("aria-label","Drag to adjust your location"),e.style.width="40px",e.style.height="52px",e.innerHTML='\n <svg\n class="qr-pin__svg"\n width="40"\n height="52"\n viewBox="0 0 40 52"\n overflow="visible"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n aria-hidden="true"\n >\n <defs>\n \x3c!--\n The filter region must be generous enough to contain the shadow blur.\n stdDeviation=3 โ†’ ~9px blur radius; our region extends 50% on each side.\n overflow="visible" on the <svg> lets it paint outside the viewport.\n --\x3e\n <filter id="qr-pin-shadow" x="-60%" y="-40%" width="220%" height="220%">\n <feDropShadow dx="0" dy="3" stdDeviation="3.5"\n flood-color="#000" flood-opacity="0.28"/>\n </filter>\n </defs>\n\n \x3c!-- Teardrop body โ€” tip is at (20, 52) = bottom-center of the SVG --\x3e\n <path\n d="M20 0C9.402 0 0 9.402 0 20C0 34 20 52 20 52S40 34 40 20C40 9.402 30.598 0 20 0Z"\n fill="#0ea5e9"\n filter="url(#qr-pin-shadow)"\n />\n \x3c!-- Dot inside the pin --\x3e\n <circle cx="20" cy="20" r="9" fill="white"/>\n <circle cx="20" cy="20" r="5" fill="#0ea5e9"/>\n </svg>\n\n \x3c!--\n Pulse ring โ€” position: absolute inside .qr-pin (position: relative).\n bottom: -(height/2) โ†’ bottom: -10px centres the 20ร—20 circle\n exactly at y = PIN_H = 52px = the pin tip coordinate.\n --\x3e\n <div class="qr-pin__pulse" aria-hidden="true"></div>\n ',e}(),t=new n.Marker({element:r,draggable:!0,anchor:"bottom"}).setLngLat([g,m]).addTo(a)
45
+ return e.className="qr-pin",e.setAttribute("aria-label","Drag to adjust your location"),e.style.width="40px",e.style.height="52px",e.innerHTML='\n <svg\n class="qr-pin__svg"\n width="40"\n height="52"\n viewBox="0 0 40 52"\n overflow="visible"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n aria-hidden="true"\n >\n <defs>\n \x3c!--\n The filter region must be generous enough to contain the shadow blur.\n stdDeviation=3 โ†’ ~9px blur radius; our region extends 50% on each side.\n overflow="visible" on the <svg> lets it paint outside the viewport.\n --\x3e\n <filter id="qr-pin-shadow" x="-60%" y="-40%" width="220%" height="220%">\n <feDropShadow dx="0" dy="3" stdDeviation="3.5"\n flood-color="#000" flood-opacity="0.28"/>\n </filter>\n </defs>\n\n \x3c!-- Teardrop body โ€” tip is at (20, 52) = bottom-center of the SVG --\x3e\n <path\n d="M20 0C9.402 0 0 9.402 0 20C0 34 20 52 20 52S40 34 40 20C40 9.402 30.598 0 20 0Z"\n fill="#0ea5e9"\n filter="url(#qr-pin-shadow)"\n />\n \x3c!-- Dot inside the pin --\x3e\n <circle cx="20" cy="20" r="9" fill="white"/>\n <circle cx="20" cy="20" r="5" fill="#0ea5e9"/>\n </svg>\n\n \x3c!--\n Pulse ring โ€” position: absolute inside .qr-pin (position: relative).\n bottom: -(height/2) โ†’ bottom: -10px centres the 20ร—20 circle\n exactly at y = PIN_H = 52px = the pin tip coordinate.\n --\x3e\n <div class="qr-pin__pulse" aria-hidden="true"></div>\n ',e}(),t=new n.Marker({element:r,draggable:!0,anchor:"bottom"}).setLngLat([q,b]).addTo(i)
45
46
  t.on("drag",()=>{const e=t.getLngLat()
46
- b(e.lat),N(e.lng),S(e.lat,e.lng)}),t.on("dragend",()=>{const e=t.getLngLat()
47
- a.easeTo({center:[e.lng,e.lat],duration:250})}),p.current=t}),a.on("click",e=>{var r
48
- const n=e.lngLat
49
- null==(r=p.current)||r.setLngLat([n.lng,n.lat]),b(n.lat),N(n.lng),S(n.lat,n.lng),a.easeTo({center:[n.lng,n.lat],duration:200})}),h.current=a,()=>{a.remove(),h.current=null,p.current=null}},[])
50
- const A=a.useCallback(()=>{E(),C((e,r)=>{var a
51
- b(e),N(r),S(e,r),h.current&&h.current.flyTo({center:[r,e],zoom:18,duration:1400,essential:!0}),null==(a=p.current)||a.setLngLat([r,e])})},[C,E,S]),T=a.useCallback(()=>{v&&e(q,y,v)},[q,y,v,e]),I=null!==v,M=I&&w
52
- return r.jsxs("div",{className:"qr-map-wrapper",children:[r.jsxs("div",{className:"qr-step-header",children:[r.jsx("div",{className:"qr-step-badge",children:"1"}),r.jsxs("div",{className:"qr-step-text",children:[r.jsx("span",{className:"qr-step-title",children:"Pin Your Location"}),r.jsx("span",{className:"qr-step-sub",children:"Tap the map or drag the pin to your exact home / office"})]})]}),r.jsxs("div",{className:"qr-map-outer",style:{height:o},children:[r.jsx("div",{ref:u,className:"qr-map-canvas"}),v&&r.jsxs("div",{className:"qr-digipin-badge","aria-live":"polite","aria-label":`DigiPin: ${v}`,children:[r.jsx("span",{className:"qr-digipin-badge__label",children:"DigiPin"}),r.jsx("span",{className:"qr-digipin-badge__code",children:v})]}),!I&&w&&r.jsx("div",{className:"qr-map-notice",role:"status",children:"Move map to India to get a DigiPin"}),r.jsx("button",{className:"qr-locate-btn"+(x?" qr-locate-btn--loading":""),onClick:A,disabled:x,title:"Use my current location","aria-label":"Use my current location",type:"button",children:x?r.jsx("span",{className:"qr-spinner","aria-hidden":"true"}):r.jsxs("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("circle",{cx:"12",cy:"12",r:"3",fill:"currentColor",stroke:"none"}),r.jsx("circle",{cx:"12",cy:"12",r:"7"}),r.jsx("path",{d:"M12 2v3M12 19v3M2 12h3M19 12h3"})]})}),L&&r.jsxs("div",{className:"qr-geo-error",role:"alert",children:[r.jsx("span",{children:L}),r.jsx("button",{type:"button",className:"qr-geo-error__dismiss",onClick:E,"aria-label":"Dismiss error",children:"ร—"})]})]}),w&&r.jsxs("div",{className:"qr-coords-strip","aria-label":"Current pin coordinates",children:[r.jsxs("span",{children:[q.toFixed(5),"ยฐ N"]}),r.jsx("span",{className:"qr-coords-sep",children:"ยท"}),r.jsxs("span",{children:[y.toFixed(5),"ยฐ E"]})]}),r.jsxs("div",{className:"qr-map-actions",children:[r.jsxs("button",{type:"button",className:"qr-btn qr-btn--primary qr-btn--full",onClick:T,disabled:!M,children:["Confirm Location",r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M5 12h14M12 5l7 7-7 7"})})]}),!I&&w&&r.jsx("p",{className:"qr-map-hint",children:"Move the pin to a location in India to continue"})]})]})},h="https://api.quantaroute.com"
53
- async function p(e,r,a,n=h){const t=`${n.replace(/\/$/,"")}/v1/location/lookup`
47
+ N(e.lat),w(e.lng),T(e.lat,e.lng)}),t.on("dragend",()=>{const e=t.getLngLat()
48
+ i.easeTo({center:[e.lng,e.lat],duration:250})}),f.current=t}),i.on("click",e=>{var r
49
+ const t=e.lngLat
50
+ null==(r=f.current)||r.setLngLat([t.lng,t.lat]),N(t.lat),w(t.lng),T(t.lat,t.lng),i.easeTo({center:[t.lng,t.lat],duration:200})}),g.current=i,()=>{i.remove(),g.current=null,f.current=null}},[])
51
+ const O=t.useCallback(()=>{A(),E((e,r)=>{var t
52
+ N(e),w(r),T(e,r),g.current&&g.current.flyTo({center:[r,e],zoom:18,duration:1400,essential:!0}),null==(t=f.current)||t.setLngLat([r,e])})},[E,A,T]),I=t.useCallback(()=>{k&&e(_,v,k)},[_,v,k,e]),D=null!==k,P=D&&S
53
+ return r.jsxs("div",{className:"qr-map-wrapper",children:[r.jsxs("div",{className:"qr-step-header",children:[r.jsx("div",{className:"qr-step-badge",children:"1"}),r.jsxs("div",{className:"qr-step-text",children:[r.jsx("span",{className:"qr-step-title",children:"Pin Your Location"}),r.jsx("span",{className:"qr-step-sub",children:"Tap the map or drag the pin to your exact home / office"})]})]}),r.jsxs("div",{className:"qr-map-outer",style:{height:o},children:[r.jsx("div",{ref:m,className:"qr-map-canvas"}),k&&r.jsxs("div",{className:"qr-digipin-badge","aria-live":"polite","aria-label":`DigiPin: ${k}`,children:[r.jsx("span",{className:"qr-digipin-badge__label",children:"DigiPin"}),r.jsx("span",{className:"qr-digipin-badge__code",children:k})]}),!D&&S&&r.jsx("div",{className:"qr-map-notice",role:"status",children:"Move map to India to get a DigiPin"}),r.jsx("button",{className:"qr-locate-btn"+(M?" qr-locate-btn--loading":""),onClick:O,disabled:M,title:"Use my current location","aria-label":"Use my current location",type:"button",children:M?r.jsx("span",{className:"qr-spinner","aria-hidden":"true"}):r.jsxs("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("circle",{cx:"12",cy:"12",r:"3",fill:"currentColor",stroke:"none"}),r.jsx("circle",{cx:"12",cy:"12",r:"7"}),r.jsx("path",{d:"M12 2v3M12 19v3M2 12h3M19 12h3"})]})}),C&&r.jsxs("div",{className:"qr-geo-error",role:"alert",children:[r.jsx("span",{children:C}),r.jsx("button",{type:"button",className:"qr-geo-error__dismiss",onClick:A,"aria-label":"Dismiss error",children:"ร—"})]})]}),S&&r.jsxs("div",{className:"qr-coords-strip","aria-label":"Current pin coordinates",children:[r.jsxs("span",{children:[_.toFixed(5),"ยฐ N"]}),r.jsx("span",{className:"qr-coords-sep",children:"ยท"}),r.jsxs("span",{children:[v.toFixed(5),"ยฐ E"]})]}),r.jsxs("div",{className:"qr-map-actions",children:[r.jsxs("button",{type:"button",className:"qr-btn qr-btn--primary qr-btn--full",onClick:I,disabled:!P,children:["Confirm Location",r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M5 12h14M12 5l7 7-7 7"})})]}),!D&&S&&r.jsx("p",{className:"qr-map-hint",children:"Move the pin to a location in India to continue"})]})]})},m="https://api.quantaroute.com"
54
+ function g(e){if("undefined"!=typeof AbortSignal&&"function"==typeof AbortSignal.timeout)return AbortSignal.timeout(e)
55
+ const r=new AbortController
56
+ return setTimeout(()=>r.abort(),e),r.signal}async function f(e,r,t,n=m){const a=`${n.replace(/\/$/,"")}/v1/location/lookup`
54
57
  let i
55
- try{i=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":a},body:JSON.stringify({latitude:e,longitude:r}),signal:AbortSignal.timeout(15e3)})}catch(l){if(l instanceof Error&&"TimeoutError"===l.name)throw new Error("Request timed out. Check your internet connection.")
58
+ try{i=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":t},body:JSON.stringify({latitude:e,longitude:r}),signal:g(15e3)})}catch(l){if(l instanceof Error&&("TimeoutError"===l.name||"AbortError"===l.name))throw new Error("Request timed out. Check your internet connection.")
56
59
  throw new Error(`Network error: ${l instanceof Error?l.message:String(l)}`)}if(!i.ok){let e=""
57
60
  try{e=await i.text()}catch{}if(401===i.status||403===i.status)throw new Error("Invalid API key. Check your QuantaRoute API key.")
58
61
  if(429===i.status)throw new Error("Rate limit exceeded. Upgrade your plan or try again later.")
59
62
  throw new Error(`API error ${i.status}: ${e||i.statusText}`)}const o=await i.json()
60
63
  if(!o.success)throw new Error(o.message??"Location lookup failed")
61
- return o}async function m(e,r,a=h){const n=`${a.replace(/\/$/,"")}/v1/digipin/reverse`
62
- let t
63
- try{t=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":r},body:JSON.stringify({digipin:e}),signal:AbortSignal.timeout(15e3)})}catch(o){if(o instanceof Error&&"TimeoutError"===o.name)throw new Error("Request timed out. Check your internet connection.")
64
- throw new Error(`Network error: ${o instanceof Error?o.message:String(o)}`)}if(!t.ok){let e=""
65
- try{e=await t.text()}catch{}if(401===t.status||403===t.status)throw new Error("Invalid API key. Check your QuantaRoute API key.")
66
- if(429===t.status)throw new Error("Rate limit exceeded. Upgrade your plan or try again later.")
67
- throw new Error(`API error ${t.status}: ${e||t.statusText}`)}const i=await t.json()
64
+ return o}async function b(e,r,t=m){const n=`${t.replace(/\/$/,"")}/v1/digipin/reverse`
65
+ let a
66
+ try{a=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":r},body:JSON.stringify({digipin:e}),signal:g(15e3)})}catch(o){if(o instanceof Error&&("TimeoutError"===o.name||"AbortError"===o.name))throw new Error("Request timed out. Check your internet connection.")
67
+ throw new Error(`Network error: ${o instanceof Error?o.message:String(o)}`)}if(!a.ok){let e=""
68
+ try{e=await a.text()}catch{}if(401===a.status||403===a.status)throw new Error("Invalid API key. Check your QuantaRoute API key.")
69
+ if(429===a.status)throw new Error("Rate limit exceeded. Upgrade your plan or try again later.")
70
+ throw new Error(`API error ${a.status}: ${e||a.statusText}`)}const i=await a.json()
68
71
  if(!i.success)throw new Error(i.message??"Reverse geocoding failed")
69
- return i}const g={flatNumber:"",floorNumber:"",buildingName:"",streetName:""}
70
- function f(e,r){switch(r.type){case"LOAD_START":return{status:"loading"}
72
+ return i}const q={flatNumber:"",floorNumber:"",buildingName:"",streetName:""}
73
+ function y(e,r){switch(r.type){case"LOAD_START":return{status:"loading"}
71
74
  case"LOAD_SUCCESS":{const e=function(e){const r={}
72
75
  e.name?r.buildingName=e.name:e.building_name?r.buildingName=e.building_name:e.addr_housename&&(r.buildingName=e.addr_housename)
73
- const a=[]
74
- return e.road&&a.push(e.road),e.suburb&&a.push(e.suburb),a.length>0&&(r.streetName=a.join(", ")),r}(r.addressComponents)
75
- return{status:"ready",adminInfo:r.adminInfo,alternatives:r.alternatives||[],selectedLocality:r.adminInfo.locality,fields:{...g,...e},submitting:!1}}case"LOAD_ERROR":return{status:"error",message:r.message}
76
+ const t=[]
77
+ return e.road&&t.push(e.road),e.suburb&&t.push(e.suburb),t.length>0&&(r.streetName=t.join(", ")),r}(r.addressComponents)
78
+ return{status:"ready",adminInfo:r.adminInfo,alternatives:r.alternatives||[],selectedLocality:r.adminInfo.locality,fields:{...q,...e},submitting:!1}}case"LOAD_ERROR":return{status:"error",message:r.message}
76
79
  case"SET_FIELD":return"ready"!==e.status?e:{...e,fields:{...e.fields,[r.key]:r.value}}
77
80
  case"SET_LOCALITY":return"ready"!==e.status?e:{...e,selectedLocality:r.locality}
78
81
  case"SUBMIT_START":return"ready"!==e.status?e:{...e,submitting:!0}
79
82
  case"SUBMIT_END":return"ready"!==e.status?e:{...e,submitting:!1}
80
- default:return e}}const q=({digipin:e,lat:n,lng:t,apiKey:i,apiBaseUrl:o,onAddressComplete:l,onBack:s,onError:d})=>{const[c,u]=a.useReducer(f,{status:"loading"}),h=a.useCallback(async()=>{u({type:"LOAD_START"})
81
- try{const[r,a]=await Promise.all([p(n,t,i,o),m(e,i,o)])
82
- u({type:"LOAD_SUCCESS",adminInfo:r.data.administrative_info,alternatives:r.data.alternatives||[],addressComponents:a.data.addressComponents})}catch(r){const e=r instanceof Error?r.message:"Failed to fetch address data."
83
- u({type:"LOAD_ERROR",message:e}),null==d||d(r instanceof Error?r:new Error(e))}},[e,n,t,i,o,d])
84
- a.useEffect(()=>{h()},[h])
85
- const g=a.useCallback(r=>{if(r.preventDefault(),"ready"!==c.status)return
86
- const{adminInfo:a,selectedLocality:i,fields:o}=c,s=[o.flatNumber,o.floorNumber?`Floor ${o.floorNumber}`:"",o.buildingName,o.streetName,i,a.district,a.state,a.pincode].filter(Boolean),d={digipin:e,lat:n,lng:t,state:a.state,district:a.district,division:a.division,locality:i,pincode:a.pincode,delivery:a.delivery,country:a.country??"India",...o,formattedAddress:s.join(", ")}
87
- u({type:"SUBMIT_START"})
88
- try{l(d)}finally{u({type:"SUBMIT_END"})}},[c,e,n,t,l])
89
- return r.jsxs("div",{className:"qr-form-wrapper",children:[r.jsxs("div",{className:"qr-step-header",children:[r.jsx("button",{type:"button",className:"qr-back-btn",onClick:s,"aria-label":"Back to map",children:r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),r.jsx("div",{className:"qr-step-badge",children:"2"}),r.jsxs("div",{className:"qr-step-text",children:[r.jsx("span",{className:"qr-step-title",children:"Add Address Details"}),r.jsx("span",{className:"qr-step-sub",children:"Flat number and building info"})]})]}),r.jsxs("div",{className:"qr-form-digipin-strip",children:[r.jsxs("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("path",{d:"M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"}),r.jsx("circle",{cx:"12",cy:"10",r:"3"})]}),r.jsx("span",{className:"qr-form-digipin-strip__label",children:"DigiPin"}),r.jsx("span",{className:"qr-form-digipin-strip__code",children:e})]}),"loading"===c.status&&r.jsxs("div",{className:"qr-loading-state","aria-busy":"true","aria-label":"Fetching address details",children:[r.jsx("div",{className:"qr-spinner qr-spinner--lg","aria-hidden":"true"}),r.jsx("p",{className:"qr-loading-state__text",children:"Fetching address detailsโ€ฆ"})]}),"error"===c.status&&r.jsxs("div",{className:"qr-error-state",role:"alert",children:[r.jsx("div",{className:"qr-error-state__icon","aria-hidden":"true",children:"โš "}),r.jsx("p",{className:"qr-error-state__msg",children:c.message}),r.jsx("button",{type:"button",className:"qr-btn qr-btn--secondary qr-btn--sm",onClick:()=>{h()},children:"Retry"})]}),"ready"===c.status&&r.jsxs("form",{onSubmit:g,className:"qr-form",noValidate:!0,children:[r.jsxs("fieldset",{className:"qr-fieldset",children:[r.jsxs("legend",{className:"qr-fieldset__legend",children:[r.jsx("span",{className:"qr-fieldset__icon","aria-hidden":"true",children:"๐Ÿ“"}),"Auto-detected from your pin"]}),r.jsxs("div",{className:"qr-auto-grid",children:[r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"State"}),r.jsx("span",{className:"qr-auto-row__value",children:c.adminInfo.state})]}),r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"District"}),r.jsx("span",{className:"qr-auto-row__value",children:c.adminInfo.district})]}),r.jsxs("div",{className:"qr-auto-row qr-auto-row--full",children:[r.jsx("span",{className:"qr-auto-row__label",children:"Locality"}),c.alternatives.length>0?r.jsxs("select",{className:"qr-auto-row__select",value:c.selectedLocality,onChange:e=>u({type:"SET_LOCALITY",locality:e.target.value}),"aria-label":"Select locality",children:[r.jsx("option",{value:c.adminInfo.locality,children:c.adminInfo.locality}),c.alternatives.map((e,a)=>r.jsx("option",{value:e.name,children:e.name},a))]}):r.jsx("span",{className:"qr-auto-row__value",children:c.selectedLocality})]}),r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"Pincode"}),r.jsx("span",{className:"qr-auto-row__value qr-auto-row__value--pin",children:c.adminInfo.pincode})]})]})]}),r.jsxs("fieldset",{className:"qr-fieldset",children:[r.jsxs("legend",{className:"qr-fieldset__legend",children:[r.jsx("span",{className:"qr-fieldset__icon","aria-hidden":"true",children:"๐Ÿ "}),"Your details"]}),r.jsxs("div",{className:"qr-fields-grid",children:[r.jsxs("div",{className:"qr-field qr-field--full",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-flatNumber",children:["Flat / House Number",r.jsx("span",{className:"qr-required","aria-hidden":"true",children:"*"})]}),r.jsx("input",{id:"qr-flatNumber",type:"text",className:"qr-field__input",placeholder:"e.g. 4B, Flat 201, House No. 12",value:c.fields.flatNumber,onChange:e=>u({type:"SET_FIELD",key:"flatNumber",value:e.target.value}),autoComplete:"address-line1",required:!0,"aria-required":"true"})]}),r.jsxs("div",{className:"qr-field",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-floorNumber",children:["Floor",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-floorNumber",type:"text",className:"qr-field__input",placeholder:"e.g. 3rd, Ground",value:c.fields.floorNumber,onChange:e=>u({type:"SET_FIELD",key:"floorNumber",value:e.target.value})})]}),r.jsxs("div",{className:"qr-field",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-buildingName",children:["Building / Society",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-buildingName",type:"text",className:"qr-field__input",placeholder:"e.g. Sunshine Apts, DDA Colony",value:c.fields.buildingName,onChange:e=>u({type:"SET_FIELD",key:"buildingName",value:e.target.value}),autoComplete:"address-line2"})]}),r.jsxs("div",{className:"qr-field qr-field--full",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-streetName",children:["Street / Area",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-streetName",type:"text",className:"qr-field__input",placeholder:"e.g. MG Road, Sector 12",value:c.fields.streetName,onChange:e=>u({type:"SET_FIELD",key:"streetName",value:e.target.value}),autoComplete:"address-level3"})]})]})]}),r.jsxs("div",{className:"qr-form-actions",children:[r.jsxs("button",{type:"button",className:"qr-btn qr-btn--ghost",onClick:s,children:[r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})}),"Adjust Pin"]}),r.jsx("button",{type:"submit",className:"qr-btn qr-btn--primary qr-btn--grow",disabled:c.submitting||!c.fields.flatNumber.trim(),children:c.submitting?r.jsxs(r.Fragment,{children:[r.jsx("span",{className:"qr-spinner qr-spinner--sm","aria-hidden":"true"}),"Savingโ€ฆ"]}):r.jsxs(r.Fragment,{children:["Save Address",r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M20 6L9 17l-5-5"})})]})})]})]})]})},b=({address:e,onEditAddress:a})=>r.jsxs("div",{className:"qr-success",children:[r.jsx("div",{className:"qr-success__icon","aria-hidden":"true",children:r.jsxs("svg",{viewBox:"0 0 52 52",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[r.jsx("circle",{cx:"26",cy:"26",r:"26",fill:"var(--qr-success, #10b981)",opacity:"0.12"}),r.jsx("circle",{cx:"26",cy:"26",r:"20",fill:"var(--qr-success, #10b981)"}),r.jsx("path",{d:"M15 26l8 8 14-16",stroke:"white",strokeWidth:"3",strokeLinecap:"round",strokeLinejoin:"round"})]})}),r.jsx("h3",{className:"qr-success__title",children:"Address Saved!"}),r.jsx("p",{className:"qr-success__address",children:e.formattedAddress}),r.jsx("div",{className:"qr-success__meta",children:r.jsxs("span",{className:"qr-digipin-badge qr-digipin-badge--inline",children:[r.jsx("span",{className:"qr-digipin-badge__label",children:"DigiPin"}),r.jsx("span",{className:"qr-digipin-badge__code",children:e.digipin})]})}),r.jsx("button",{type:"button",className:"qr-btn qr-btn--ghost qr-btn--sm",onClick:a,children:"Change Address"})]}),y=({apiKey:e,apiBaseUrl:n="https://api.quantaroute.com",onComplete:t,onError:i,defaultLat:o,defaultLng:l,theme:s="light",className:d="",style:c,mapHeight:h="380px",title:p="Add Delivery Address",indiaBoundaryUrl:m})=>{const[g,f]=a.useState("map"),[y,N]=a.useState(null),[v,_]=a.useState(null)
90
- return r.jsxs("div",{className:`qr-checkout qr-checkout--${s} ${d}`,style:c,"data-testid":"quantaroute-checkout",children:["done"!==g&&r.jsxs("div",{className:"qr-header",children:[r.jsxs("div",{className:"qr-header__brand",children:[r.jsxs("svg",{className:"qr-header__logo","aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("path",{d:"M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"}),r.jsx("circle",{cx:"12",cy:"10",r:"3"})]}),r.jsx("span",{className:"qr-header__title",children:p})]}),r.jsxs("div",{className:"qr-progress","aria-label":"Progress: step ${ step === 'map' ? 1 : 2 } of 2",role:"progressbar",children:[r.jsx("div",{className:"qr-progress__dot "+("map"===g||"form"===g||"done"===g?"qr-progress__dot--active":"")}),r.jsx("div",{className:"qr-progress__line "+("form"===g||"done"===g?"qr-progress__line--active":"")}),r.jsx("div",{className:"qr-progress__dot "+("form"===g||"done"===g?"qr-progress__dot--active":"")})]})]}),"map"===g&&r.jsx(u,{onLocationConfirm:(e,r,a)=>{N({lat:e,lng:r,digipin:a}),f("form")},defaultLat:o,defaultLng:l,mapHeight:h,theme:s,indiaBoundaryUrl:m}),"form"===g&&y&&r.jsx(q,{digipin:y.digipin,lat:y.lat,lng:y.lng,apiKey:e,apiBaseUrl:n,onAddressComplete:e=>{_(e),f("done"),t(e)},onBack:()=>{f("map")},onError:i,theme:s}),"done"===g&&v&&r.jsx(b,{address:v,onEditAddress:()=>{f("map"),N(null),_(null)}}),r.jsxs("div",{className:"qr-footer",children:[r.jsx("span",{children:"Powered by"}),r.jsx("a",{href:"https://quantaroute.com",target:"_blank",rel:"noopener noreferrer",className:"qr-footer__link",children:"QuantaRoute"}),r.jsx("span",{className:"qr-footer__flag","aria-label":"Made in India",children:"๐Ÿ‡ฎ๐Ÿ‡ณ"})]})]})}
91
- e.AddressForm=q,e.CheckoutWidget=y,e.DIGIPIN_BOUNDS=i,e.MapPinSelector=u,e.default=y,e.getDigiPin=o,e.getLatLngFromDigiPin=l,e.isValidDigiPin=function(e){if(!e||"string"!=typeof e)return!1
83
+ default:return e}}const _=({digipin:e,lat:n,lng:a,apiKey:i,apiBaseUrl:o,onAddressComplete:l,onBack:s,onError:c})=>{const[d,p]=t.useReducer(y,{status:"loading"}),u=t.useCallback(async()=>{p({type:"LOAD_START"})
84
+ try{const[r,t]=await Promise.all([f(n,a,i,o),b(e,i,o)])
85
+ p({type:"LOAD_SUCCESS",adminInfo:r.data.administrative_info,alternatives:r.data.alternatives||[],addressComponents:t.data.addressComponents})}catch(r){const e=r instanceof Error?r.message:"Failed to fetch address data."
86
+ p({type:"LOAD_ERROR",message:e}),null==c||c(r instanceof Error?r:new Error(e))}},[e,n,a,i,o,c])
87
+ t.useEffect(()=>{u()},[u])
88
+ const h=t.useCallback(r=>{if(r.preventDefault(),"ready"!==d.status)return
89
+ const{adminInfo:t,selectedLocality:i,fields:o}=d,s=[o.flatNumber,o.floorNumber?`Floor ${o.floorNumber}`:"",o.buildingName,o.streetName,i,t.district,t.state,t.pincode].filter(Boolean),c={digipin:e,lat:n,lng:a,state:t.state,district:t.district,division:t.division,locality:i,pincode:t.pincode,delivery:t.delivery,country:t.country??"India",...o,formattedAddress:s.join(", ")}
90
+ p({type:"SUBMIT_START"})
91
+ try{l(c)}finally{p({type:"SUBMIT_END"})}},[d,e,n,a,l])
92
+ return r.jsxs("div",{className:"qr-form-wrapper",children:[r.jsxs("div",{className:"qr-step-header",children:[r.jsx("button",{type:"button",className:"qr-back-btn",onClick:s,"aria-label":"Back to map",children:r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})})}),r.jsx("div",{className:"qr-step-badge",children:"2"}),r.jsxs("div",{className:"qr-step-text",children:[r.jsx("span",{className:"qr-step-title",children:"Add Address Details"}),r.jsx("span",{className:"qr-step-sub",children:"Flat number and building info"})]})]}),r.jsxs("div",{className:"qr-form-digipin-strip",children:[r.jsxs("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("path",{d:"M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"}),r.jsx("circle",{cx:"12",cy:"10",r:"3"})]}),r.jsx("span",{className:"qr-form-digipin-strip__label",children:"DigiPin"}),r.jsx("span",{className:"qr-form-digipin-strip__code",children:e})]}),"loading"===d.status&&r.jsxs("div",{className:"qr-loading-state","aria-busy":"true","aria-label":"Fetching address details",children:[r.jsx("div",{className:"qr-spinner qr-spinner--lg","aria-hidden":"true"}),r.jsx("p",{className:"qr-loading-state__text",children:"Fetching address detailsโ€ฆ"})]}),"error"===d.status&&r.jsxs("div",{className:"qr-error-state",role:"alert",children:[r.jsx("div",{className:"qr-error-state__icon","aria-hidden":"true",children:"โš "}),r.jsx("p",{className:"qr-error-state__msg",children:d.message}),r.jsx("button",{type:"button",className:"qr-btn qr-btn--secondary qr-btn--sm",onClick:()=>{u()},children:"Retry"})]}),"ready"===d.status&&r.jsxs("form",{onSubmit:h,className:"qr-form",noValidate:!0,children:[r.jsxs("fieldset",{className:"qr-fieldset",children:[r.jsxs("legend",{className:"qr-fieldset__legend",children:[r.jsx("span",{className:"qr-fieldset__icon","aria-hidden":"true",children:"๐Ÿ“"}),"Auto-detected from your pin"]}),r.jsxs("div",{className:"qr-auto-grid",children:[r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"State"}),r.jsx("span",{className:"qr-auto-row__value",children:d.adminInfo.state})]}),r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"District"}),r.jsx("span",{className:"qr-auto-row__value",children:d.adminInfo.district})]}),r.jsxs("div",{className:"qr-auto-row qr-auto-row--full",children:[r.jsx("span",{className:"qr-auto-row__label",children:"Locality"}),d.alternatives.length>0?r.jsxs("select",{className:"qr-auto-row__select",value:d.selectedLocality,onChange:e=>p({type:"SET_LOCALITY",locality:e.target.value}),"aria-label":"Select locality",children:[r.jsx("option",{value:d.adminInfo.locality,children:d.adminInfo.locality}),d.alternatives.map((e,t)=>r.jsx("option",{value:e.name,children:e.name},t))]}):r.jsx("span",{className:"qr-auto-row__value",children:d.selectedLocality})]}),r.jsxs("div",{className:"qr-auto-row",children:[r.jsx("span",{className:"qr-auto-row__label",children:"Pincode"}),r.jsx("span",{className:"qr-auto-row__value qr-auto-row__value--pin",children:d.adminInfo.pincode})]})]})]}),r.jsxs("fieldset",{className:"qr-fieldset",children:[r.jsxs("legend",{className:"qr-fieldset__legend",children:[r.jsx("span",{className:"qr-fieldset__icon","aria-hidden":"true",children:"๐Ÿ "}),"Your details"]}),r.jsxs("div",{className:"qr-fields-grid",children:[r.jsxs("div",{className:"qr-field qr-field--full",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-flatNumber",children:["Flat / House Number",r.jsx("span",{className:"qr-required","aria-hidden":"true",children:"*"})]}),r.jsx("input",{id:"qr-flatNumber",type:"text",className:"qr-field__input",placeholder:"e.g. 4B, Flat 201, House No. 12",value:d.fields.flatNumber,onChange:e=>p({type:"SET_FIELD",key:"flatNumber",value:e.target.value}),autoComplete:"address-line1",required:!0,"aria-required":"true"})]}),r.jsxs("div",{className:"qr-field",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-floorNumber",children:["Floor",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-floorNumber",type:"text",className:"qr-field__input",placeholder:"e.g. 3rd, Ground",value:d.fields.floorNumber,onChange:e=>p({type:"SET_FIELD",key:"floorNumber",value:e.target.value})})]}),r.jsxs("div",{className:"qr-field",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-buildingName",children:["Building / Society",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-buildingName",type:"text",className:"qr-field__input",placeholder:"e.g. Sunshine Apts, DDA Colony",value:d.fields.buildingName,onChange:e=>p({type:"SET_FIELD",key:"buildingName",value:e.target.value}),autoComplete:"address-line2"})]}),r.jsxs("div",{className:"qr-field qr-field--full",children:[r.jsxs("label",{className:"qr-field__label",htmlFor:"qr-streetName",children:["Street / Area",r.jsx("span",{className:"qr-optional",children:"optional"})]}),r.jsx("input",{id:"qr-streetName",type:"text",className:"qr-field__input",placeholder:"e.g. MG Road, Sector 12",value:d.fields.streetName,onChange:e=>p({type:"SET_FIELD",key:"streetName",value:e.target.value}),autoComplete:"address-level3"})]})]})]}),r.jsxs("div",{className:"qr-form-actions",children:[r.jsxs("button",{type:"button",className:"qr-btn qr-btn--ghost",onClick:s,children:[r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M19 12H5M12 19l-7-7 7-7"})}),"Adjust Pin"]}),r.jsx("button",{type:"submit",className:"qr-btn qr-btn--primary qr-btn--grow",disabled:d.submitting||!d.fields.flatNumber.trim(),children:d.submitting?r.jsxs(r.Fragment,{children:[r.jsx("span",{className:"qr-spinner qr-spinner--sm","aria-hidden":"true"}),"Savingโ€ฆ"]}):r.jsxs(r.Fragment,{children:["Save Address",r.jsx("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:r.jsx("path",{d:"M20 6L9 17l-5-5"})})]})})]})]})]})},N=({address:e,onEditAddress:t})=>r.jsxs("div",{className:"qr-success",children:[r.jsx("div",{className:"qr-success__icon","aria-hidden":"true",children:r.jsxs("svg",{viewBox:"0 0 52 52",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[r.jsx("circle",{cx:"26",cy:"26",r:"26",fill:"var(--qr-success, #10b981)",opacity:"0.12"}),r.jsx("circle",{cx:"26",cy:"26",r:"20",fill:"var(--qr-success, #10b981)"}),r.jsx("path",{d:"M15 26l8 8 14-16",stroke:"white",strokeWidth:"3",strokeLinecap:"round",strokeLinejoin:"round"})]})}),r.jsx("h3",{className:"qr-success__title",children:"Address Saved!"}),r.jsx("p",{className:"qr-success__address",children:e.formattedAddress}),r.jsx("div",{className:"qr-success__meta",children:r.jsxs("span",{className:"qr-digipin-badge qr-digipin-badge--inline",children:[r.jsx("span",{className:"qr-digipin-badge__label",children:"DigiPin"}),r.jsx("span",{className:"qr-digipin-badge__code",children:e.digipin})]})}),r.jsx("button",{type:"button",className:"qr-btn qr-btn--ghost qr-btn--sm",onClick:t,children:"Change Address"})]}),v=({apiKey:e,apiBaseUrl:n="https://api.quantaroute.com",onComplete:a,onError:i,defaultLat:o,defaultLng:l,theme:s="light",className:c="",style:d,mapHeight:p="380px",title:u="Add Delivery Address",mapStyle:m,indiaBoundaryUrl:g})=>{const[f,b]=t.useState("map"),[q,y]=t.useState(null),[v,w]=t.useState(null)
93
+ return r.jsxs("div",{className:`qr-checkout qr-checkout--${s} ${c}`,style:d,"data-testid":"quantaroute-checkout",children:["done"!==f&&r.jsxs("div",{className:"qr-header",children:[r.jsxs("div",{className:"qr-header__brand",children:[r.jsxs("svg",{className:"qr-header__logo","aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[r.jsx("path",{d:"M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"}),r.jsx("circle",{cx:"12",cy:"10",r:"3"})]}),r.jsx("span",{className:"qr-header__title",children:u})]}),r.jsxs("div",{className:"qr-progress","aria-label":"Progress: step ${ step === 'map' ? 1 : 2 } of 2",role:"progressbar",children:[r.jsx("div",{className:"qr-progress__dot "+("map"===f||"form"===f||"done"===f?"qr-progress__dot--active":"")}),r.jsx("div",{className:"qr-progress__line "+("form"===f||"done"===f?"qr-progress__line--active":"")}),r.jsx("div",{className:"qr-progress__dot "+("form"===f||"done"===f?"qr-progress__dot--active":"")})]})]}),"map"===f&&r.jsx(h,{onLocationConfirm:(e,r,t)=>{y({lat:e,lng:r,digipin:t}),b("form")},defaultLat:o,defaultLng:l,mapHeight:p,theme:s,mapStyle:m,indiaBoundaryUrl:g}),"form"===f&&q&&r.jsx(_,{digipin:q.digipin,lat:q.lat,lng:q.lng,apiKey:e,apiBaseUrl:n,onAddressComplete:e=>{w(e),b("done"),a(e)},onBack:()=>{b("map")},onError:i,theme:s}),"done"===f&&v&&r.jsx(N,{address:v,onEditAddress:()=>{b("map"),y(null),w(null)}}),r.jsxs("div",{className:"qr-footer",children:[r.jsx("span",{children:"Powered by"}),r.jsx("a",{href:"https://quantaroute.com",target:"_blank",rel:"noopener noreferrer",className:"qr-footer__link",children:"QuantaRoute"}),r.jsx("span",{className:"qr-footer__flag","aria-label":"Made in India",children:"๐Ÿ‡ฎ๐Ÿ‡ณ"})]})]})}
94
+ e.AddressForm=_,e.CheckoutWidget=v,e.DIGIPIN_BOUNDS=i,e.MapPinSelector=h,e.default=v,e.getDigiPin=o,e.getLatLngFromDigiPin=l,e.isValidDigiPin=function(e){if(!e||"string"!=typeof e)return!1
92
95
  if(!/^[A-Z0-9]{3}-[A-Z0-9]{3}-[A-Z0-9]{4}$/i.test(e.trim()))return!1
93
- try{return l(e.trim().toUpperCase()),!0}catch{return!1}},e.isWithinIndia=s,e.lookupLocation=p,e.reverseGeocode=m,e.useDigiPin=function(e,r){return a.useMemo(()=>{if(!s(e,r))return null
94
- try{return o(e,r)}catch{return null}},[e,r])},e.useGeolocation=d,Object.defineProperties(e,{t:{value:!0},[Symbol.toStringTag]:{value:"Module"}})})
96
+ try{return l(e.trim().toUpperCase()),!0}catch{return!1}},e.isWithinIndia=s,e.lookupLocation=f,e.reverseGeocode=b,e.useDigiPin=function(e,r){return t.useMemo(()=>{if(!s(e,r))return null
97
+ try{return o(e,r)}catch{return null}},[e,r])},e.useGeolocation=c,Object.defineProperties(e,{t:{value:!0},[Symbol.toStringTag]:{value:"Module"}})})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantaroute/checkout",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Embeddable DigiPin-powered smart address checkout widget โ€” React (web), React Native & Expo (iOS/Android)",
5
5
  "main": "./dist/lib/quantaroute-checkout.umd.js",
6
6
  "module": "./dist/lib/quantaroute-checkout.es.js",
@@ -74,6 +74,7 @@ const CheckoutWidget: React.FC<CheckoutWidgetProps> = ({
74
74
  style,
75
75
  mapHeight = '380px',
76
76
  title = 'Add Delivery Address',
77
+ mapStyle,
77
78
  indiaBoundaryUrl,
78
79
  }) => {
79
80
  const [step, setStep] = useState<Step>('map');
@@ -152,6 +153,7 @@ const CheckoutWidget: React.FC<CheckoutWidgetProps> = ({
152
153
  defaultLng={defaultLng}
153
154
  mapHeight={mapHeight}
154
155
  theme={theme}
156
+ mapStyle={mapStyle}
155
157
  indiaBoundaryUrl={indiaBoundaryUrl}
156
158
  />
157
159
  )}
@@ -3,13 +3,13 @@ import {
3
3
  View,
4
4
  Text,
5
5
  TouchableOpacity,
6
- ActivityIndicator,
7
6
  } from 'react-native';
8
- import { OSMView } from 'expo-osm-sdk';
7
+ import { OSMView, LocationButton } from 'expo-osm-sdk';
9
8
  import type { OSMViewRef, MarkerConfig } from 'expo-osm-sdk';
9
+ import * as Location from 'expo-location';
10
10
  import { getDigiPin, isWithinIndia } from '../core/digipin';
11
- import { useGeolocation } from '../hooks/useGeolocation.native';
12
11
  import type { MapPinSelectorProps } from '../core/types';
12
+ import { resolveMapStyle } from '../core/mapStyles';
13
13
  import { styles, COLORS } from '../styles/checkout.native';
14
14
 
15
15
  // โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -18,12 +18,7 @@ const INDIA_CENTER = { latitude: 20.5937, longitude: 78.9629 };
18
18
  const OVERVIEW_ZOOM = 5;
19
19
  const STREET_ZOOM = 16;
20
20
 
21
- /**
22
- * Carto Positron vector basemap โ€” same source as the web version.
23
- * Free, no API key required. Attribution required (shown by OSMView natively).
24
- */
25
- const CARTO_STYLE_URL =
26
- 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
21
+ // Style URL is resolved per-instance from the mapStyle prop (see resolveMapStyle).
27
22
 
28
23
  /**
29
24
  * Custom pin SVG as a data URI โ€” matches the web pin colour (#0ea5e9).
@@ -63,6 +58,7 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
63
58
  defaultLng,
64
59
  mapHeight = 380,
65
60
  theme = 'light',
61
+ mapStyle,
66
62
  }) => {
67
63
  const mapRef = useRef<OSMViewRef>(null);
68
64
 
@@ -75,10 +71,10 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
75
71
  computeDigiPinSafe(initLat, initLng)
76
72
  );
77
73
  const [mapReady, setMapReady] = useState(false);
78
-
79
- const { loading: geoLoading, error: geoError, locate, clearError } = useGeolocation();
74
+ const [locateError, setLocateError] = useState<string | null>(null);
80
75
 
81
76
  const isDark = theme === 'dark';
77
+ const { url: styleUrl } = resolveMapStyle(mapStyle);
82
78
 
83
79
  // Resolve numeric height: web passes '380px', native should pass 380 or a number
84
80
  const mapHeightNum =
@@ -107,14 +103,18 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
107
103
  },
108
104
  ];
109
105
 
110
- // โ”€โ”€ Locate-me handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
- const handleLocateMe = useCallback(() => {
112
- clearError();
113
- locate((lat, lng) => {
114
- handleCoordChange(lat, lng);
115
- void mapRef.current?.animateToLocation(lat, lng, STREET_ZOOM);
106
+ // โ”€โ”€ Locate-me: delegate to expo-osm-sdk's LocationButton via getCurrentLocation โ”€โ”€
107
+ const getCurrentLocation = useCallback(async () => {
108
+ setLocateError(null);
109
+ const { status } = await Location.requestForegroundPermissionsAsync();
110
+ if (status !== Location.PermissionStatus.GRANTED) {
111
+ throw new Error('Location permission denied');
112
+ }
113
+ const loc = await Location.getCurrentPositionAsync({
114
+ accuracy: Location.Accuracy.High,
116
115
  });
117
- }, [locate, clearError, handleCoordChange]);
116
+ return { latitude: loc.coords.latitude, longitude: loc.coords.longitude };
117
+ }, []);
118
118
 
119
119
  // โ”€โ”€ Confirm handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
120
120
  const handleConfirm = useCallback(() => {
@@ -151,7 +151,7 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
151
151
  style={{ flex: 1 }}
152
152
  initialCenter={{ latitude: initLat, longitude: initLng }}
153
153
  initialZoom={hasDefault ? STREET_ZOOM : OVERVIEW_ZOOM}
154
- styleUrl={CARTO_STYLE_URL}
154
+ styleUrl={styleUrl}
155
155
  markers={pinMarkers}
156
156
  showUserLocation={false}
157
157
  scrollEnabled
@@ -186,31 +186,26 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
186
186
  </View>
187
187
  ) : null}
188
188
 
189
- {/* Locate-me button */}
190
- <TouchableOpacity
191
- style={[styles.locateBtn, isDark && styles.locateBtnDark]}
192
- onPress={handleLocateMe}
193
- disabled={geoLoading}
194
- activeOpacity={0.7}
195
- accessibilityLabel="Use my current location"
196
- accessibilityRole="button"
197
- >
198
- {geoLoading ? (
199
- <ActivityIndicator size="small" color={COLORS.primary} />
200
- ) : (
201
- <Text style={{ fontSize: 20, color: isDark ? COLORS.textDark : COLORS.text }}>
202
- โŠ•
203
- </Text>
204
- )}
205
- </TouchableOpacity>
189
+ {/* Locate-me button โ€” expo-osm-sdk's built-in LocationButton */}
190
+ <LocationButton
191
+ style={styles.locateBtn}
192
+ color={COLORS.primary}
193
+ size={44}
194
+ getCurrentLocation={getCurrentLocation}
195
+ onLocationFound={({ latitude, longitude }) => {
196
+ handleCoordChange(latitude, longitude);
197
+ void mapRef.current?.animateToLocation(latitude, longitude, STREET_ZOOM);
198
+ }}
199
+ onLocationError={(err) => setLocateError(err)}
200
+ />
206
201
 
207
202
  {/* Geo error toast */}
208
- {geoError ? (
203
+ {locateError ? (
209
204
  <View style={styles.geoError}>
210
- <Text style={styles.geoErrorText}>{geoError}</Text>
205
+ <Text style={styles.geoErrorText}>{locateError}</Text>
211
206
  <TouchableOpacity
212
207
  style={styles.geoErrorDismiss}
213
- onPress={clearError}
208
+ onPress={() => setLocateError(null)}
214
209
  accessibilityLabel="Dismiss error"
215
210
  >
216
211
  <Text style={styles.geoErrorDismissText}>ร—</Text>
@@ -4,6 +4,7 @@ import 'maplibre-gl/dist/maplibre-gl.css';
4
4
  import { getDigiPin, isWithinIndia } from '../core/digipin';
5
5
  import { useGeolocation } from '../hooks/useGeolocation';
6
6
  import type { MapPinSelectorProps } from '../core/types';
7
+ import { resolveMapStyle } from '../core/mapStyles';
7
8
 
8
9
  // โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
9
10
 
@@ -13,13 +14,7 @@ const MIN_ZOOM = 6; // users cannot zoom out beyond a useful India-level v
13
14
  const OVERVIEW_ZOOM = 9;
14
15
  const STREET_ZOOM = 18;
15
16
 
16
- /**
17
- * Carto Positron โ€“ free vector basemap, no API key required.
18
- * Attribution: ยฉ OpenStreetMap contributors ยฉ CARTO
19
- * Terms: https://carto.com/legal/
20
- */
21
- const CARTO_STYLE_URL =
22
- 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
17
+ // Style URL is resolved per-instance from the mapStyle prop (see resolveMapStyle).
23
18
 
24
19
  // โ”€โ”€โ”€ Pin dimensions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
25
20
  // MUST match the SVG width/height exactly.
@@ -103,6 +98,7 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
103
98
  defaultLng,
104
99
  mapHeight = '380px',
105
100
  theme: _theme = 'light', // kept for future dark-mode pin tint; unused for now
101
+ mapStyle,
106
102
  indiaBoundaryUrl,
107
103
  }) => {
108
104
  const containerRef = useRef<HTMLDivElement>(null);
@@ -144,10 +140,11 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
144
140
  : Promise.resolve(null);
145
141
 
146
142
  const initZoom = hasDefault ? STREET_ZOOM : OVERVIEW_ZOOM;
143
+ const { url: styleUrl, attributionHtml } = resolveMapStyle(mapStyle);
147
144
 
148
145
  const map = new maplibregl.Map({
149
146
  container: containerRef.current,
150
- style: CARTO_STYLE_URL,
147
+ style: styleUrl,
151
148
  center: [initLng, initLat],
152
149
  zoom: initZoom,
153
150
  minZoom: MIN_ZOOM, // lock: cannot zoom out past level 7
@@ -156,12 +153,11 @@ const MapPinSelector: React.FC<MapPinSelectorProps> = ({
156
153
  touchZoomRotate: true,
157
154
  });
158
155
 
159
- // Attribution (required by Carto & OSM terms)
156
+ // Attribution (required by OSM + each basemap provider's terms)
160
157
  map.addControl(
161
158
  new maplibregl.AttributionControl({
162
159
  compact: true,
163
- customAttribution:
164
- 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>',
160
+ customAttribution: attributionHtml,
165
161
  }),
166
162
  'bottom-right'
167
163
  );
package/src/core/api.ts CHANGED
@@ -2,6 +2,23 @@ import type { LocationLookupResponse } from './types';
2
2
 
3
3
  const DEFAULT_BASE_URL = 'https://api.quantaroute.com';
4
4
 
5
+ /**
6
+ * Cross-platform timeout signal.
7
+ * AbortSignal.timeout() is not available in React Native / Hermes โ€” fall back
8
+ * to a manual AbortController + setTimeout when it is missing.
9
+ */
10
+ function makeTimeoutSignal(ms: number): AbortSignal {
11
+ if (
12
+ typeof AbortSignal !== 'undefined' &&
13
+ typeof (AbortSignal as { timeout?: unknown }).timeout === 'function'
14
+ ) {
15
+ return AbortSignal.timeout(ms);
16
+ }
17
+ const ctrl = new AbortController();
18
+ setTimeout(() => ctrl.abort(), ms);
19
+ return ctrl.signal;
20
+ }
21
+
5
22
  /**
6
23
  * Address components from Nominatim/OpenStreetMap (via reverse geocoding).
7
24
  */
@@ -61,10 +78,10 @@ export async function lookupLocation(
61
78
  'x-api-key': apiKey,
62
79
  },
63
80
  body: JSON.stringify({ latitude: lat, longitude: lng }),
64
- signal: AbortSignal.timeout(15_000), // 15s timeout
81
+ signal: makeTimeoutSignal(15_000),
65
82
  });
66
83
  } catch (err) {
67
- if (err instanceof Error && err.name === 'TimeoutError') {
84
+ if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) {
68
85
  throw new Error('Request timed out. Check your internet connection.');
69
86
  }
70
87
  throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
@@ -115,10 +132,10 @@ export async function reverseGeocode(
115
132
  'x-api-key': apiKey,
116
133
  },
117
134
  body: JSON.stringify({ digipin }),
118
- signal: AbortSignal.timeout(15_000), // 15s timeout
135
+ signal: makeTimeoutSignal(15_000),
119
136
  });
120
137
  } catch (err) {
121
- if (err instanceof Error && err.name === 'TimeoutError') {
138
+ if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) {
122
139
  throw new Error('Request timed out. Check your internet connection.');
123
140
  }
124
141
  throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
@@ -0,0 +1,74 @@
1
+ import type { BuiltInMapStyle } from './types';
2
+
3
+ // โ”€โ”€โ”€ Style registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4
+
5
+ interface StyleEntry {
6
+ url: string;
7
+ /** HTML attribution string (web). Plain text for native. */
8
+ attributionHtml: string;
9
+ attributionPlain: string;
10
+ }
11
+
12
+ const STYLE_REGISTRY: Record<BuiltInMapStyle, StyleEntry> = {
13
+ 'carto-positron': {
14
+ url: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
15
+ attributionHtml:
16
+ 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors' +
17
+ ' &nbsp;ยฉ <a href="https://carto.com/attributions" target="_blank" rel="noopener">CARTO</a>',
18
+ attributionPlain: 'ยฉ OpenStreetMap contributors ยฉ CARTO',
19
+ },
20
+ 'openfreemap-liberty': {
21
+ url: 'https://tiles.openfreemap.org/styles/liberty',
22
+ attributionHtml:
23
+ 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors' +
24
+ ' &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
25
+ ' &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
26
+ attributionPlain: 'ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap',
27
+ },
28
+ 'openfreemap-positron': {
29
+ url: 'https://tiles.openfreemap.org/styles/positron',
30
+ attributionHtml:
31
+ 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors' +
32
+ ' &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
33
+ ' &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
34
+ attributionPlain: 'ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap',
35
+ },
36
+ 'openfreemap-bright': {
37
+ url: 'https://tiles.openfreemap.org/styles/bright',
38
+ attributionHtml:
39
+ 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors' +
40
+ ' &nbsp;ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
41
+ ' &nbsp;<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a>',
42
+ attributionPlain: 'ยฉ OpenStreetMap contributors ยฉ OpenMapTiles ยท OpenFreeMap',
43
+ },
44
+ };
45
+
46
+ // โ”€โ”€โ”€ Resolver โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
47
+
48
+ const DEFAULT_STYLE: BuiltInMapStyle = 'carto-positron';
49
+
50
+ function isBuiltIn(value: string): value is BuiltInMapStyle {
51
+ return value in STYLE_REGISTRY;
52
+ }
53
+
54
+ /**
55
+ * Resolves a mapStyle prop value to its concrete style URL and attribution.
56
+ * If the value is a preset name, returns the registered entry.
57
+ * If the value is a custom URL string, returns that URL with generic OSM attribution.
58
+ * If undefined, falls back to the default preset (carto-positron).
59
+ */
60
+ export function resolveMapStyle(mapStyle: BuiltInMapStyle | string | undefined): StyleEntry {
61
+ if (!mapStyle || mapStyle === DEFAULT_STYLE) {
62
+ return STYLE_REGISTRY[DEFAULT_STYLE];
63
+ }
64
+ if (isBuiltIn(mapStyle)) {
65
+ return STYLE_REGISTRY[mapStyle];
66
+ }
67
+ // Custom URL โ€” caller is responsible for setting correct attribution via their style JSON
68
+ return {
69
+ url: mapStyle,
70
+ attributionHtml:
71
+ 'ยฉ <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors',
72
+ attributionPlain: 'ยฉ OpenStreetMap contributors',
73
+ };
74
+ }
package/src/core/types.ts CHANGED
@@ -83,6 +83,29 @@ export interface CompleteAddress {
83
83
  formattedAddress: string;
84
84
  }
85
85
 
86
+ // โ”€โ”€โ”€ Map Style โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+
88
+ /**
89
+ * Built-in basemap style presets.
90
+ *
91
+ * | Preset | Provider | Character |
92
+ * |--------------------------|---------------|----------------------------------|
93
+ * | `'carto-positron'` | Carto | Clean, minimal, light (default) |
94
+ * | `'openfreemap-liberty'` | OpenFreeMap | Colorful OSM-flavored |
95
+ * | `'openfreemap-positron'` | OpenFreeMap | Clean light, open-source |
96
+ * | `'openfreemap-bright'` | OpenFreeMap | Vibrant, high contrast |
97
+ *
98
+ * You can also pass any MapLibre-compatible style URL string directly.
99
+ *
100
+ * OpenFreeMap is free and open-source with no API key or rate limits.
101
+ * See https://openfreemap.org
102
+ */
103
+ export type BuiltInMapStyle =
104
+ | 'carto-positron'
105
+ | 'openfreemap-liberty'
106
+ | 'openfreemap-positron'
107
+ | 'openfreemap-bright';
108
+
86
109
  // โ”€โ”€โ”€ Component Props โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
110
 
88
111
  export interface MapPinSelectorProps {
@@ -92,6 +115,13 @@ export interface MapPinSelectorProps {
92
115
  /** Map height. String on web ('380px'), number on native (380). */
93
116
  mapHeight?: string | number;
94
117
  theme?: 'light' | 'dark';
118
+ /**
119
+ * Basemap style. Pass a preset name or any MapLibre-compatible style URL.
120
+ * Defaults to `'carto-positron'`.
121
+ * @example mapStyle="openfreemap-liberty"
122
+ * @example mapStyle="https://my-server.com/style.json"
123
+ */
124
+ mapStyle?: BuiltInMapStyle | string;
95
125
  /**
96
126
  * URL to an India boundary GeoJSON file (FeatureCollection).
97
127
  * When provided, a thin grey outline of India's border is drawn on the map
@@ -140,6 +170,13 @@ export interface CheckoutWidgetProps {
140
170
  mapHeight?: string | number;
141
171
  /** Widget header title. Defaults to 'Add Delivery Address'. */
142
172
  title?: string;
173
+ /**
174
+ * Basemap style. Pass a preset name or any MapLibre-compatible style URL.
175
+ * Defaults to `'carto-positron'`.
176
+ * @example mapStyle="openfreemap-liberty"
177
+ * @example mapStyle="https://my-server.com/style.json"
178
+ */
179
+ mapStyle?: BuiltInMapStyle | string;
143
180
  /**
144
181
  * URL to an India boundary GeoJSON file (FeatureCollection).
145
182
  * When provided, a thin grey outline is drawn on the map for legal compliance.