@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 +43 -6
- package/dist/lib/index.d.ts +31 -0
- package/dist/lib/quantaroute-checkout.es.js +57 -9
- package/dist/lib/quantaroute-checkout.umd.js +56 -53
- package/package.json +1 -1
- package/src/components/CheckoutWidget.tsx +2 -0
- package/src/components/MapPinSelector.native.tsx +34 -39
- package/src/components/MapPinSelector.tsx +7 -11
- package/src/core/api.ts +21 -4
- package/src/core/mapStyles.ts +74 -0
- package/src/core/types.ts +37 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://quantaroute.com)
|
|
8
8
|
[](https://www.indiapost.gov.in)
|
|
9
|
-
[](https://openfreemap.org)
|
|
10
10
|
[](./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
|
|
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
|
-
|
|
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
|
-
-
|
|
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)
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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 ยฉ <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
26
|
-
for(let i=1;i<=10;i++){const
|
|
27
|
-
let
|
|
28
|
-
|
|
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
|
|
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,
|
|
33
|
-
for(let r=0;r<4;r++){for(let
|
|
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
|
|
36
|
-
|
|
37
|
-
return{...e,locate:
|
|
38
|
-
e.code===e.PERMISSION_DENIED?
|
|
39
|
-
|
|
40
|
-
a.
|
|
41
|
-
|
|
42
|
-
|
|
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 ยฉ <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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 ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a> <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([
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
null==(r=
|
|
50
|
-
const
|
|
51
|
-
|
|
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:
|
|
53
|
-
|
|
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(
|
|
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
|
|
62
|
-
let
|
|
63
|
-
try{
|
|
64
|
-
throw new Error(`Network error: ${o instanceof Error?o.message:String(o)}`)}if(!
|
|
65
|
-
try{e=await
|
|
66
|
-
if(429===
|
|
67
|
-
throw new Error(`API error ${
|
|
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
|
|
70
|
-
function
|
|
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
|
|
74
|
-
return e.road&&
|
|
75
|
-
return{status:"ready",adminInfo:r.adminInfo,alternatives:r.alternatives||[],selectedLocality:r.adminInfo.locality,fields:{...
|
|
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
|
|
81
|
-
try{const[r,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const{adminInfo:
|
|
87
|
-
|
|
88
|
-
try{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"===
|
|
90
|
-
return r.jsxs("div",{className:`qr-checkout qr-checkout--${s} ${
|
|
91
|
-
e.AddressForm=
|
|
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=
|
|
94
|
-
try{return o(e,r)}catch{return null}},[e,r])},e.useGeolocation=
|
|
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.
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
<
|
|
191
|
-
style={
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
{
|
|
203
|
+
{locateError ? (
|
|
209
204
|
<View style={styles.geoError}>
|
|
210
|
-
<Text style={styles.geoErrorText}>{
|
|
205
|
+
<Text style={styles.geoErrorText}>{locateError}</Text>
|
|
211
206
|
<TouchableOpacity
|
|
212
207
|
style={styles.geoErrorDismiss}
|
|
213
|
-
onPress={
|
|
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:
|
|
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
|
|
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 ยฉ <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:
|
|
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:
|
|
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
|
+
' ยฉ <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
|
+
' ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
|
|
25
|
+
' <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
|
+
' ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
|
|
33
|
+
' <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
|
+
' ยฉ <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>' +
|
|
41
|
+
' <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.
|