@quantaroute/checkout 1.1.1 → 1.2.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/LICENSE +29 -0
- package/README.md +299 -172
- package/babel.config.js +16 -0
- package/dist/lib/index.d.ts +9 -7
- package/dist/lib/quantaroute-checkout.es.js +9 -3
- package/dist/lib/quantaroute-checkout.umd.js +9 -3
- package/expo-plugin.js +109 -0
- package/package.json +47 -10
- package/src/components/AddressForm.native.tsx +540 -0
- package/src/components/AddressForm.tsx +477 -0
- package/src/components/CheckoutWidget.native.tsx +218 -0
- package/src/components/CheckoutWidget.tsx +196 -0
- package/src/components/MapPinSelector.native.tsx +254 -0
- package/src/components/MapPinSelector.tsx +405 -0
- package/src/core/api.ts +150 -0
- package/src/core/digipin.ts +169 -0
- package/src/core/types.ts +150 -0
- package/src/hooks/useDigiPin.ts +20 -0
- package/src/hooks/useGeolocation.native.ts +55 -0
- package/src/hooks/useGeolocation.ts +48 -0
- package/src/index.ts +59 -0
- package/src/styles/checkout.css +1082 -0
- package/src/styles/checkout.native.ts +839 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import MapPinSelector from './MapPinSelector';
|
|
3
|
+
import AddressForm from './AddressForm';
|
|
4
|
+
import type { CheckoutWidgetProps, CompleteAddress } from '../core/types';
|
|
5
|
+
import '../styles/checkout.css';
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
type Step = 'map' | 'form' | 'done';
|
|
10
|
+
|
|
11
|
+
interface ConfirmedLocation {
|
|
12
|
+
lat: number;
|
|
13
|
+
lng: number;
|
|
14
|
+
digipin: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Success screen ───────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface SuccessScreenProps {
|
|
20
|
+
address: CompleteAddress;
|
|
21
|
+
onEditAddress: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SuccessScreen: React.FC<SuccessScreenProps> = ({ address, onEditAddress }) => (
|
|
25
|
+
<div className="qr-success">
|
|
26
|
+
<div className="qr-success__icon" aria-hidden="true">
|
|
27
|
+
<svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
28
|
+
<circle cx="26" cy="26" r="26" fill="var(--qr-success, #10b981)" opacity="0.12" />
|
|
29
|
+
<circle cx="26" cy="26" r="20" fill="var(--qr-success, #10b981)" />
|
|
30
|
+
<path
|
|
31
|
+
d="M15 26l8 8 14-16"
|
|
32
|
+
stroke="white"
|
|
33
|
+
strokeWidth="3"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
</div>
|
|
39
|
+
<h3 className="qr-success__title">Address Saved!</h3>
|
|
40
|
+
<p className="qr-success__address">{address.formattedAddress}</p>
|
|
41
|
+
<div className="qr-success__meta">
|
|
42
|
+
<span className="qr-digipin-badge qr-digipin-badge--inline">
|
|
43
|
+
<span className="qr-digipin-badge__label">DigiPin</span>
|
|
44
|
+
<span className="qr-digipin-badge__code">{address.digipin}</span>
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<button type="button" className="qr-btn qr-btn--ghost qr-btn--sm" onClick={onEditAddress}>
|
|
48
|
+
Change Address
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// ─── Main Widget ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* QuantaRoute Checkout Widget
|
|
57
|
+
*
|
|
58
|
+
* A two-step, mobile-first address collection component:
|
|
59
|
+
* Step 1 – Map: user pins their exact location (DigiPin shown offline, real-time)
|
|
60
|
+
* Step 2 – Form: auto-filled address + manual flat/building details
|
|
61
|
+
*
|
|
62
|
+
* Works in React, Next.js, Nuxt (via nuxt-module), Vite, etc.
|
|
63
|
+
* Embed as a script tag (vanilla JS) via the UMD build.
|
|
64
|
+
*/
|
|
65
|
+
const CheckoutWidget: React.FC<CheckoutWidgetProps> = ({
|
|
66
|
+
apiKey,
|
|
67
|
+
apiBaseUrl = 'https://api.quantaroute.com',
|
|
68
|
+
onComplete,
|
|
69
|
+
onError,
|
|
70
|
+
defaultLat,
|
|
71
|
+
defaultLng,
|
|
72
|
+
theme = 'light',
|
|
73
|
+
className = '',
|
|
74
|
+
style,
|
|
75
|
+
mapHeight = '380px',
|
|
76
|
+
title = 'Add Delivery Address',
|
|
77
|
+
indiaBoundaryUrl,
|
|
78
|
+
}) => {
|
|
79
|
+
const [step, setStep] = useState<Step>('map');
|
|
80
|
+
const [confirmedLocation, setConfirmedLocation] = useState<ConfirmedLocation | null>(null);
|
|
81
|
+
const [completedAddress, setCompletedAddress] = useState<CompleteAddress | null>(null);
|
|
82
|
+
|
|
83
|
+
// ── Handlers ──────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
const handleLocationConfirm = (lat: number, lng: number, digipin: string) => {
|
|
86
|
+
setConfirmedLocation({ lat, lng, digipin });
|
|
87
|
+
setStep('form');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleAddressComplete = (address: CompleteAddress) => {
|
|
91
|
+
setCompletedAddress(address);
|
|
92
|
+
setStep('done');
|
|
93
|
+
onComplete(address);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleBack = () => {
|
|
97
|
+
setStep('map');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleEditAddress = () => {
|
|
101
|
+
setStep('map');
|
|
102
|
+
setConfirmedLocation(null);
|
|
103
|
+
setCompletedAddress(null);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Step indicator ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const renderStepDots = () => (
|
|
109
|
+
<div className="qr-progress" aria-label="Progress: step ${ step === 'map' ? 1 : 2 } of 2" role="progressbar">
|
|
110
|
+
<div className={`qr-progress__dot ${step === 'map' || step === 'form' || step === 'done' ? 'qr-progress__dot--active' : ''}`} />
|
|
111
|
+
<div className={`qr-progress__line ${step === 'form' || step === 'done' ? 'qr-progress__line--active' : ''}`} />
|
|
112
|
+
<div className={`qr-progress__dot ${step === 'form' || step === 'done' ? 'qr-progress__dot--active' : ''}`} />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
className={`qr-checkout qr-checkout--${theme} ${className}`}
|
|
121
|
+
style={style}
|
|
122
|
+
data-testid="quantaroute-checkout"
|
|
123
|
+
>
|
|
124
|
+
{/* Widget header */}
|
|
125
|
+
{step !== 'done' && (
|
|
126
|
+
<div className="qr-header">
|
|
127
|
+
<div className="qr-header__brand">
|
|
128
|
+
<svg
|
|
129
|
+
className="qr-header__logo"
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
strokeWidth="2"
|
|
135
|
+
strokeLinecap="round"
|
|
136
|
+
strokeLinejoin="round"
|
|
137
|
+
>
|
|
138
|
+
<path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z" />
|
|
139
|
+
<circle cx="12" cy="10" r="3" />
|
|
140
|
+
</svg>
|
|
141
|
+
<span className="qr-header__title">{title}</span>
|
|
142
|
+
</div>
|
|
143
|
+
{renderStepDots()}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* ── Step 1: Map ── */}
|
|
148
|
+
{step === 'map' && (
|
|
149
|
+
<MapPinSelector
|
|
150
|
+
onLocationConfirm={handleLocationConfirm}
|
|
151
|
+
defaultLat={defaultLat}
|
|
152
|
+
defaultLng={defaultLng}
|
|
153
|
+
mapHeight={mapHeight}
|
|
154
|
+
theme={theme}
|
|
155
|
+
indiaBoundaryUrl={indiaBoundaryUrl}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* ── Step 2: Form ── */}
|
|
160
|
+
{step === 'form' && confirmedLocation && (
|
|
161
|
+
<AddressForm
|
|
162
|
+
digipin={confirmedLocation.digipin}
|
|
163
|
+
lat={confirmedLocation.lat}
|
|
164
|
+
lng={confirmedLocation.lng}
|
|
165
|
+
apiKey={apiKey}
|
|
166
|
+
apiBaseUrl={apiBaseUrl}
|
|
167
|
+
onAddressComplete={handleAddressComplete}
|
|
168
|
+
onBack={handleBack}
|
|
169
|
+
onError={onError}
|
|
170
|
+
theme={theme}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* ── Step 3: Done ── */}
|
|
175
|
+
{step === 'done' && completedAddress && (
|
|
176
|
+
<SuccessScreen address={completedAddress} onEditAddress={handleEditAddress} />
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* Powered-by footer */}
|
|
180
|
+
<div className="qr-footer">
|
|
181
|
+
<span>Powered by</span>
|
|
182
|
+
<a
|
|
183
|
+
href="https://quantaroute.com"
|
|
184
|
+
target="_blank"
|
|
185
|
+
rel="noopener noreferrer"
|
|
186
|
+
className="qr-footer__link"
|
|
187
|
+
>
|
|
188
|
+
QuantaRoute
|
|
189
|
+
</a>
|
|
190
|
+
<span className="qr-footer__flag" aria-label="Made in India">🇮🇳</span>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default CheckoutWidget;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React, { useRef, useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { OSMView } from 'expo-osm-sdk';
|
|
9
|
+
import type { OSMViewRef, MarkerConfig } from 'expo-osm-sdk';
|
|
10
|
+
import { getDigiPin, isWithinIndia } from '../core/digipin';
|
|
11
|
+
import { useGeolocation } from '../hooks/useGeolocation.native';
|
|
12
|
+
import type { MapPinSelectorProps } from '../core/types';
|
|
13
|
+
import { styles, COLORS } from '../styles/checkout.native';
|
|
14
|
+
|
|
15
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const INDIA_CENTER = { latitude: 20.5937, longitude: 78.9629 };
|
|
18
|
+
const OVERVIEW_ZOOM = 5;
|
|
19
|
+
const STREET_ZOOM = 16;
|
|
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';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Custom pin SVG as a data URI — matches the web pin colour (#0ea5e9).
|
|
30
|
+
* Teardrop shape with white inner circle and blue dot.
|
|
31
|
+
*/
|
|
32
|
+
const PIN_ICON_URI =
|
|
33
|
+
"data:image/svg+xml;utf8," +
|
|
34
|
+
encodeURIComponent(
|
|
35
|
+
'<svg width="40" height="52" viewBox="0 0 40 52" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
|
36
|
+
'<path 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" fill="#0ea5e9"/>' +
|
|
37
|
+
'<circle cx="20" cy="20" r="9" fill="white"/>' +
|
|
38
|
+
'<circle cx="20" cy="20" r="5" fill="#0ea5e9"/>' +
|
|
39
|
+
'</svg>'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function computeDigiPinSafe(lat: number, lng: number): string | null {
|
|
45
|
+
if (!isWithinIndia(lat, lng)) return null;
|
|
46
|
+
try {
|
|
47
|
+
return getDigiPin(lat, lng);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Native (iOS/Android) map pin selector.
|
|
57
|
+
* Uses expo-osm-sdk's OSMView with a draggable marker over Carto Positron tiles.
|
|
58
|
+
* Metro resolves this file on native; MapPinSelector.tsx is used on web.
|
|
59
|
+
*/
|
|
60
|
+
const MapPinSelector: React.FC<MapPinSelectorProps> = ({
|
|
61
|
+
onLocationConfirm,
|
|
62
|
+
defaultLat,
|
|
63
|
+
defaultLng,
|
|
64
|
+
mapHeight = 380,
|
|
65
|
+
theme = 'light',
|
|
66
|
+
}) => {
|
|
67
|
+
const mapRef = useRef<OSMViewRef>(null);
|
|
68
|
+
|
|
69
|
+
const initLat = defaultLat ?? INDIA_CENTER.latitude;
|
|
70
|
+
const initLng = defaultLng ?? INDIA_CENTER.longitude;
|
|
71
|
+
const hasDefault = defaultLat !== undefined && defaultLng !== undefined;
|
|
72
|
+
|
|
73
|
+
const [coord, setCoord] = useState({ latitude: initLat, longitude: initLng });
|
|
74
|
+
const [digipin, setDigipin] = useState<string | null>(() =>
|
|
75
|
+
computeDigiPinSafe(initLat, initLng)
|
|
76
|
+
);
|
|
77
|
+
const [mapReady, setMapReady] = useState(false);
|
|
78
|
+
|
|
79
|
+
const { loading: geoLoading, error: geoError, locate, clearError } = useGeolocation();
|
|
80
|
+
|
|
81
|
+
const isDark = theme === 'dark';
|
|
82
|
+
|
|
83
|
+
// Resolve numeric height: web passes '380px', native should pass 380 or a number
|
|
84
|
+
const mapHeightNum =
|
|
85
|
+
typeof mapHeight === 'string' ? parseInt(mapHeight, 10) || 380 : mapHeight;
|
|
86
|
+
|
|
87
|
+
// ── Coordinate update (pin drag or tap) ─────────────────────────────────────
|
|
88
|
+
const handleCoordChange = useCallback(
|
|
89
|
+
(latitude: number, longitude: number) => {
|
|
90
|
+
setCoord({ latitude, longitude });
|
|
91
|
+
setDigipin(computeDigiPinSafe(latitude, longitude));
|
|
92
|
+
},
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// ── Marker config (draggable checkout pin) ──────────────────────────────────
|
|
97
|
+
const pinMarkers: MarkerConfig[] = [
|
|
98
|
+
{
|
|
99
|
+
id: 'checkout-pin',
|
|
100
|
+
coordinate: coord,
|
|
101
|
+
draggable: true,
|
|
102
|
+
icon: {
|
|
103
|
+
uri: PIN_ICON_URI,
|
|
104
|
+
size: 40,
|
|
105
|
+
anchor: { x: 0.5, y: 1.0 }, // tip of the teardrop
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
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);
|
|
116
|
+
});
|
|
117
|
+
}, [locate, clearError, handleCoordChange]);
|
|
118
|
+
|
|
119
|
+
// ── Confirm handler ──────────────────────────────────────────────────────────
|
|
120
|
+
const handleConfirm = useCallback(() => {
|
|
121
|
+
if (digipin) {
|
|
122
|
+
onLocationConfirm(coord.latitude, coord.longitude, digipin);
|
|
123
|
+
}
|
|
124
|
+
}, [coord, digipin, onLocationConfirm]);
|
|
125
|
+
|
|
126
|
+
const isInIndia = digipin !== null;
|
|
127
|
+
const canConfirm = isInIndia && mapReady;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<View style={[styles.mapWrapper, isDark && { backgroundColor: COLORS.bgDark }]}>
|
|
131
|
+
|
|
132
|
+
{/* ── Step header ── */}
|
|
133
|
+
<View style={[styles.stepHeader, isDark && styles.stepHeaderDark]}>
|
|
134
|
+
<View style={styles.stepBadge}>
|
|
135
|
+
<Text style={styles.stepBadgeText}>1</Text>
|
|
136
|
+
</View>
|
|
137
|
+
<View style={styles.stepText}>
|
|
138
|
+
<Text style={[styles.stepTitle, isDark && styles.textLight]}>
|
|
139
|
+
Pin Your Location
|
|
140
|
+
</Text>
|
|
141
|
+
<Text style={[styles.stepSub, isDark && styles.textMutedStyle]}>
|
|
142
|
+
Tap the map or drag the pin to your exact location
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{/* ── Map area ── */}
|
|
148
|
+
<View style={{ height: mapHeightNum, position: 'relative' }}>
|
|
149
|
+
<OSMView
|
|
150
|
+
ref={mapRef}
|
|
151
|
+
style={{ flex: 1 }}
|
|
152
|
+
initialCenter={{ latitude: initLat, longitude: initLng }}
|
|
153
|
+
initialZoom={hasDefault ? STREET_ZOOM : OVERVIEW_ZOOM}
|
|
154
|
+
styleUrl={CARTO_STYLE_URL}
|
|
155
|
+
markers={pinMarkers}
|
|
156
|
+
showUserLocation={false}
|
|
157
|
+
scrollEnabled
|
|
158
|
+
zoomEnabled
|
|
159
|
+
rotateEnabled
|
|
160
|
+
onMapReady={() => setMapReady(true)}
|
|
161
|
+
onPress={(coordinate) => {
|
|
162
|
+
handleCoordChange(coordinate.latitude, coordinate.longitude);
|
|
163
|
+
}}
|
|
164
|
+
onMarkerDragEnd={(_markerId, coordinate) => {
|
|
165
|
+
handleCoordChange(coordinate.latitude, coordinate.longitude);
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* DigiPin badge overlay */}
|
|
170
|
+
{digipin ? (
|
|
171
|
+
<View
|
|
172
|
+
style={[styles.digipinBadge, isDark && styles.digipinBadgeDark]}
|
|
173
|
+
pointerEvents="none"
|
|
174
|
+
>
|
|
175
|
+
<Text style={[styles.digipinLabel, isDark && styles.digipinLabelDark]}>
|
|
176
|
+
DigiPin
|
|
177
|
+
</Text>
|
|
178
|
+
<Text style={styles.digipinCode}>{digipin}</Text>
|
|
179
|
+
</View>
|
|
180
|
+
) : null}
|
|
181
|
+
|
|
182
|
+
{/* Out-of-India notice */}
|
|
183
|
+
{!isInIndia && mapReady ? (
|
|
184
|
+
<View style={styles.mapNotice} pointerEvents="none">
|
|
185
|
+
<Text style={styles.mapNoticeText}>Move map to India to get a DigiPin</Text>
|
|
186
|
+
</View>
|
|
187
|
+
) : null}
|
|
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>
|
|
206
|
+
|
|
207
|
+
{/* Geo error toast */}
|
|
208
|
+
{geoError ? (
|
|
209
|
+
<View style={styles.geoError}>
|
|
210
|
+
<Text style={styles.geoErrorText}>{geoError}</Text>
|
|
211
|
+
<TouchableOpacity
|
|
212
|
+
style={styles.geoErrorDismiss}
|
|
213
|
+
onPress={clearError}
|
|
214
|
+
accessibilityLabel="Dismiss error"
|
|
215
|
+
>
|
|
216
|
+
<Text style={styles.geoErrorDismissText}>×</Text>
|
|
217
|
+
</TouchableOpacity>
|
|
218
|
+
</View>
|
|
219
|
+
) : null}
|
|
220
|
+
</View>
|
|
221
|
+
|
|
222
|
+
{/* ── Coordinates strip ── */}
|
|
223
|
+
{mapReady ? (
|
|
224
|
+
<View style={[styles.coordsStrip, isDark && styles.coordsStripDark]}>
|
|
225
|
+
<Text style={[styles.coordsText, isDark && styles.coordsTextDark]}>
|
|
226
|
+
{coord.latitude.toFixed(5)}° N · {coord.longitude.toFixed(5)}° E
|
|
227
|
+
</Text>
|
|
228
|
+
</View>
|
|
229
|
+
) : null}
|
|
230
|
+
|
|
231
|
+
{/* ── Confirm action bar ── */}
|
|
232
|
+
<View style={[styles.mapActions, isDark && styles.mapActionsDark]}>
|
|
233
|
+
<TouchableOpacity
|
|
234
|
+
style={[styles.btnPrimary, !canConfirm && styles.btnDisabled]}
|
|
235
|
+
onPress={handleConfirm}
|
|
236
|
+
disabled={!canConfirm}
|
|
237
|
+
activeOpacity={0.8}
|
|
238
|
+
accessibilityRole="button"
|
|
239
|
+
accessibilityLabel="Confirm location"
|
|
240
|
+
>
|
|
241
|
+
<Text style={styles.btnPrimaryText}>Confirm Location →</Text>
|
|
242
|
+
</TouchableOpacity>
|
|
243
|
+
|
|
244
|
+
{!isInIndia && mapReady ? (
|
|
245
|
+
<Text style={[styles.mapHint, isDark && styles.mapHintDark]}>
|
|
246
|
+
Move the pin to a location in India to continue
|
|
247
|
+
</Text>
|
|
248
|
+
) : null}
|
|
249
|
+
</View>
|
|
250
|
+
</View>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export default MapPinSelector;
|