@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.
@@ -0,0 +1,477 @@
1
+ import React, { useEffect, useReducer, useCallback } from 'react';
2
+ import { lookupLocation, reverseGeocode, type AddressComponents } from '../core/api';
3
+ import type {
4
+ AddressFormProps,
5
+ AdministrativeInfo,
6
+ CompleteAddress,
7
+ LocationAlternative,
8
+ } from '../core/types';
9
+
10
+ // ─── State ────────────────────────────────────────────────────────────────────
11
+
12
+ interface ManualFields {
13
+ flatNumber: string;
14
+ floorNumber: string;
15
+ buildingName: string;
16
+ streetName: string;
17
+ }
18
+
19
+ type FormState =
20
+ | { status: 'loading' }
21
+ | { status: 'error'; message: string }
22
+ | {
23
+ status: 'ready';
24
+ adminInfo: AdministrativeInfo;
25
+ alternatives: LocationAlternative[];
26
+ selectedLocality: string; // Selected locality name (from adminInfo or alternatives)
27
+ fields: ManualFields;
28
+ submitting: boolean;
29
+ };
30
+
31
+ type FormAction =
32
+ | { type: 'LOAD_START' }
33
+ | {
34
+ type: 'LOAD_SUCCESS';
35
+ adminInfo: AdministrativeInfo;
36
+ alternatives: LocationAlternative[];
37
+ addressComponents: AddressComponents;
38
+ }
39
+ | { type: 'LOAD_ERROR'; message: string }
40
+ | { type: 'SET_FIELD'; key: keyof ManualFields; value: string }
41
+ | { type: 'SET_LOCALITY'; locality: string }
42
+ | { type: 'SUBMIT_START' }
43
+ | { type: 'SUBMIT_END' };
44
+
45
+ const INITIAL_FIELDS: ManualFields = {
46
+ flatNumber: '',
47
+ floorNumber: '',
48
+ buildingName: '',
49
+ streetName: '',
50
+ };
51
+
52
+ /**
53
+ * Parse addressComponents from Nominatim/OpenStreetMap.
54
+ * - name → Building/Society/POI name
55
+ * - road + suburb → Street/Area
56
+ */
57
+ function parseAddressComponents(components: AddressComponents): Partial<ManualFields> {
58
+ const fields: Partial<ManualFields> = {};
59
+
60
+ // Building/Society/POI name: prefer 'name' field (most relevant)
61
+ if (components.name) {
62
+ fields.buildingName = components.name;
63
+ } else if (components.building_name) {
64
+ fields.buildingName = components.building_name;
65
+ } else if (components.addr_housename) {
66
+ fields.buildingName = components.addr_housename;
67
+ }
68
+
69
+ // Street/Area: combine road + suburb (if available)
70
+ const streetParts: string[] = [];
71
+ if (components.road) {
72
+ streetParts.push(components.road);
73
+ }
74
+ if (components.suburb) {
75
+ streetParts.push(components.suburb);
76
+ }
77
+ if (streetParts.length > 0) {
78
+ fields.streetName = streetParts.join(', ');
79
+ }
80
+
81
+ return fields;
82
+ }
83
+
84
+ function reducer(state: FormState, action: FormAction): FormState {
85
+ switch (action.type) {
86
+ case 'LOAD_START':
87
+ return { status: 'loading' };
88
+ case 'LOAD_SUCCESS': {
89
+ // Pre-fill fields from addressComponents
90
+ const preFilled = parseAddressComponents(action.addressComponents);
91
+ // Default to primary locality from adminInfo
92
+ return {
93
+ status: 'ready',
94
+ adminInfo: action.adminInfo,
95
+ alternatives: action.alternatives || [],
96
+ selectedLocality: action.adminInfo.locality,
97
+ fields: { ...INITIAL_FIELDS, ...preFilled },
98
+ submitting: false,
99
+ };
100
+ }
101
+ case 'LOAD_ERROR':
102
+ return { status: 'error', message: action.message };
103
+ case 'SET_FIELD':
104
+ if (state.status !== 'ready') return state;
105
+ return { ...state, fields: { ...state.fields, [action.key]: action.value } };
106
+ case 'SET_LOCALITY':
107
+ if (state.status !== 'ready') return state;
108
+ return { ...state, selectedLocality: action.locality };
109
+ case 'SUBMIT_START':
110
+ if (state.status !== 'ready') return state;
111
+ return { ...state, submitting: true };
112
+ case 'SUBMIT_END':
113
+ if (state.status !== 'ready') return state;
114
+ return { ...state, submitting: false };
115
+ default:
116
+ return state;
117
+ }
118
+ }
119
+
120
+ // ─── Component ────────────────────────────────────────────────────────────────
121
+
122
+ const AddressForm: React.FC<AddressFormProps> = ({
123
+ digipin,
124
+ lat,
125
+ lng,
126
+ apiKey,
127
+ apiBaseUrl,
128
+ onAddressComplete,
129
+ onBack,
130
+ onError,
131
+ }) => {
132
+ const [state, dispatch] = useReducer(reducer, { status: 'loading' });
133
+
134
+ // ── Fetch administrative data + address components on mount ────────────────
135
+ const fetchAdminInfo = useCallback(async () => {
136
+ dispatch({ type: 'LOAD_START' });
137
+ try {
138
+ // Call both APIs in parallel:
139
+ // 1. /v1/location/lookup → administrative_info (state, district, pincode) + alternatives
140
+ // 2. /v1/digipin/reverse → addressComponents (name, road, suburb from Nominatim)
141
+ const [adminRes, reverseRes] = await Promise.all([
142
+ lookupLocation(lat, lng, apiKey, apiBaseUrl),
143
+ reverseGeocode(digipin, apiKey, apiBaseUrl),
144
+ ]);
145
+
146
+ dispatch({
147
+ type: 'LOAD_SUCCESS',
148
+ adminInfo: adminRes.data.administrative_info,
149
+ alternatives: adminRes.data.alternatives || [],
150
+ addressComponents: reverseRes.data.addressComponents,
151
+ });
152
+ } catch (err) {
153
+ const msg = err instanceof Error ? err.message : 'Failed to fetch address data.';
154
+ dispatch({ type: 'LOAD_ERROR', message: msg });
155
+ onError?.(err instanceof Error ? err : new Error(msg));
156
+ }
157
+ }, [digipin, lat, lng, apiKey, apiBaseUrl, onError]);
158
+
159
+ useEffect(() => {
160
+ void fetchAdminInfo();
161
+ }, [fetchAdminInfo]);
162
+
163
+ // ── Submit ────────────────────────────────────────────────────────────────
164
+ const handleSubmit = useCallback(
165
+ (e: React.FormEvent<HTMLFormElement>) => {
166
+ e.preventDefault();
167
+ if (state.status !== 'ready') return;
168
+
169
+ const { adminInfo, selectedLocality, fields } = state;
170
+
171
+ const parts: string[] = [
172
+ fields.flatNumber,
173
+ fields.floorNumber ? `Floor ${fields.floorNumber}` : '',
174
+ fields.buildingName,
175
+ fields.streetName,
176
+ selectedLocality, // Use selected locality (may be from alternatives)
177
+ adminInfo.district,
178
+ adminInfo.state,
179
+ adminInfo.pincode,
180
+ ].filter(Boolean);
181
+
182
+ const complete: CompleteAddress = {
183
+ digipin,
184
+ lat,
185
+ lng,
186
+ state: adminInfo.state,
187
+ district: adminInfo.district,
188
+ division: adminInfo.division,
189
+ locality: selectedLocality, // Use selected locality
190
+ pincode: adminInfo.pincode,
191
+ delivery: adminInfo.delivery,
192
+ country: adminInfo.country ?? 'India',
193
+ ...fields,
194
+ formattedAddress: parts.join(', '),
195
+ };
196
+
197
+ dispatch({ type: 'SUBMIT_START' });
198
+ try {
199
+ onAddressComplete(complete);
200
+ } finally {
201
+ dispatch({ type: 'SUBMIT_END' });
202
+ }
203
+ },
204
+ [state, digipin, lat, lng, onAddressComplete]
205
+ );
206
+
207
+ return (
208
+ <div className="qr-form-wrapper">
209
+ {/* ── Step header ── */}
210
+ <div className="qr-step-header">
211
+ <button
212
+ type="button"
213
+ className="qr-back-btn"
214
+ onClick={onBack}
215
+ aria-label="Back to map"
216
+ >
217
+ <svg
218
+ aria-hidden="true"
219
+ viewBox="0 0 24 24"
220
+ fill="none"
221
+ stroke="currentColor"
222
+ strokeWidth="2.5"
223
+ strokeLinecap="round"
224
+ strokeLinejoin="round"
225
+ >
226
+ <path d="M19 12H5M12 19l-7-7 7-7" />
227
+ </svg>
228
+ </button>
229
+ <div className="qr-step-badge">2</div>
230
+ <div className="qr-step-text">
231
+ <span className="qr-step-title">Add Address Details</span>
232
+ <span className="qr-step-sub">Flat number and building info</span>
233
+ </div>
234
+ </div>
235
+
236
+ {/* ── DigiPin reference strip ── */}
237
+ <div className="qr-form-digipin-strip">
238
+ <svg
239
+ aria-hidden="true"
240
+ viewBox="0 0 24 24"
241
+ fill="none"
242
+ stroke="currentColor"
243
+ strokeWidth="2"
244
+ strokeLinecap="round"
245
+ strokeLinejoin="round"
246
+ >
247
+ <path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z" />
248
+ <circle cx="12" cy="10" r="3" />
249
+ </svg>
250
+ <span className="qr-form-digipin-strip__label">DigiPin</span>
251
+ <span className="qr-form-digipin-strip__code">{digipin}</span>
252
+ </div>
253
+
254
+ {/* ── Loading skeleton ── */}
255
+ {state.status === 'loading' && (
256
+ <div className="qr-loading-state" aria-busy="true" aria-label="Fetching address details">
257
+ <div className="qr-spinner qr-spinner--lg" aria-hidden="true" />
258
+ <p className="qr-loading-state__text">Fetching address details…</p>
259
+ </div>
260
+ )}
261
+
262
+ {/* ── Error state ── */}
263
+ {state.status === 'error' && (
264
+ <div className="qr-error-state" role="alert">
265
+ <div className="qr-error-state__icon" aria-hidden="true">⚠</div>
266
+ <p className="qr-error-state__msg">{state.message}</p>
267
+ <button
268
+ type="button"
269
+ className="qr-btn qr-btn--secondary qr-btn--sm"
270
+ onClick={() => void fetchAdminInfo()}
271
+ >
272
+ Retry
273
+ </button>
274
+ </div>
275
+ )}
276
+
277
+ {/* ── Main form ── */}
278
+ {state.status === 'ready' && (
279
+ <form onSubmit={handleSubmit} className="qr-form" noValidate>
280
+ {/* Auto-filled section */}
281
+ <fieldset className="qr-fieldset">
282
+ <legend className="qr-fieldset__legend">
283
+ <span className="qr-fieldset__icon" aria-hidden="true">📍</span>
284
+ Auto-detected from your pin
285
+ </legend>
286
+
287
+ <div className="qr-auto-grid">
288
+ {/* State */}
289
+ <div className="qr-auto-row">
290
+ <span className="qr-auto-row__label">State</span>
291
+ <span className="qr-auto-row__value">{state.adminInfo.state}</span>
292
+ </div>
293
+
294
+ {/* District */}
295
+ <div className="qr-auto-row">
296
+ <span className="qr-auto-row__label">District</span>
297
+ <span className="qr-auto-row__value">{state.adminInfo.district}</span>
298
+ </div>
299
+
300
+ {/* Locality - show dropdown if alternatives exist */}
301
+ <div className="qr-auto-row qr-auto-row--full">
302
+ <span className="qr-auto-row__label">Locality</span>
303
+ {state.alternatives.length > 0 ? (
304
+ <select
305
+ className="qr-auto-row__select"
306
+ value={state.selectedLocality}
307
+ onChange={(e) => dispatch({ type: 'SET_LOCALITY', locality: e.target.value })}
308
+ aria-label="Select locality"
309
+ >
310
+ {/* Primary option (from adminInfo) */}
311
+ <option value={state.adminInfo.locality}>
312
+ {state.adminInfo.locality}
313
+ </option>
314
+ {/* Alternatives */}
315
+ {state.alternatives.map((alt, idx) => (
316
+ <option key={idx} value={alt.name}>
317
+ {alt.name}
318
+ </option>
319
+ ))}
320
+ </select>
321
+ ) : (
322
+ <span className="qr-auto-row__value">{state.selectedLocality}</span>
323
+ )}
324
+ </div>
325
+
326
+ {/* Pincode */}
327
+ <div className="qr-auto-row">
328
+ <span className="qr-auto-row__label">Pincode</span>
329
+ <span className="qr-auto-row__value qr-auto-row__value--pin">
330
+ {state.adminInfo.pincode}
331
+ </span>
332
+ </div>
333
+ </div>
334
+ </fieldset>
335
+
336
+ {/* Manual entry section */}
337
+ <fieldset className="qr-fieldset">
338
+ <legend className="qr-fieldset__legend">
339
+ <span className="qr-fieldset__icon" aria-hidden="true">🏠</span>
340
+ Your details
341
+ </legend>
342
+
343
+ <div className="qr-fields-grid">
344
+ {/* Flat / House – required */}
345
+ <div className="qr-field qr-field--full">
346
+ <label className="qr-field__label" htmlFor="qr-flatNumber">
347
+ Flat / House Number
348
+ <span className="qr-required" aria-hidden="true">*</span>
349
+ </label>
350
+ <input
351
+ id="qr-flatNumber"
352
+ type="text"
353
+ className="qr-field__input"
354
+ placeholder="e.g. 4B, Flat 201, House No. 12"
355
+ value={state.fields.flatNumber}
356
+ onChange={(e) =>
357
+ dispatch({ type: 'SET_FIELD', key: 'flatNumber', value: e.target.value })
358
+ }
359
+ autoComplete="address-line1"
360
+ required
361
+ aria-required="true"
362
+ />
363
+ </div>
364
+
365
+ {/* Floor */}
366
+ <div className="qr-field">
367
+ <label className="qr-field__label" htmlFor="qr-floorNumber">
368
+ Floor
369
+ <span className="qr-optional">optional</span>
370
+ </label>
371
+ <input
372
+ id="qr-floorNumber"
373
+ type="text"
374
+ className="qr-field__input"
375
+ placeholder="e.g. 3rd, Ground"
376
+ value={state.fields.floorNumber}
377
+ onChange={(e) =>
378
+ dispatch({ type: 'SET_FIELD', key: 'floorNumber', value: e.target.value })
379
+ }
380
+ />
381
+ </div>
382
+
383
+ {/* Building / Society */}
384
+ <div className="qr-field">
385
+ <label className="qr-field__label" htmlFor="qr-buildingName">
386
+ Building / Society
387
+ <span className="qr-optional">optional</span>
388
+ </label>
389
+ <input
390
+ id="qr-buildingName"
391
+ type="text"
392
+ className="qr-field__input"
393
+ placeholder="e.g. Sunshine Apts, DDA Colony"
394
+ value={state.fields.buildingName}
395
+ onChange={(e) =>
396
+ dispatch({ type: 'SET_FIELD', key: 'buildingName', value: e.target.value })
397
+ }
398
+ autoComplete="address-line2"
399
+ />
400
+ </div>
401
+
402
+ {/* Street / Area */}
403
+ <div className="qr-field qr-field--full">
404
+ <label className="qr-field__label" htmlFor="qr-streetName">
405
+ Street / Area
406
+ <span className="qr-optional">optional</span>
407
+ </label>
408
+ <input
409
+ id="qr-streetName"
410
+ type="text"
411
+ className="qr-field__input"
412
+ placeholder="e.g. MG Road, Sector 12"
413
+ value={state.fields.streetName}
414
+ onChange={(e) =>
415
+ dispatch({ type: 'SET_FIELD', key: 'streetName', value: e.target.value })
416
+ }
417
+ autoComplete="address-level3"
418
+ />
419
+ </div>
420
+ </div>
421
+ </fieldset>
422
+
423
+ {/* Actions */}
424
+ <div className="qr-form-actions">
425
+ <button
426
+ type="button"
427
+ className="qr-btn qr-btn--ghost"
428
+ onClick={onBack}
429
+ >
430
+ <svg
431
+ aria-hidden="true"
432
+ viewBox="0 0 24 24"
433
+ fill="none"
434
+ stroke="currentColor"
435
+ strokeWidth="2"
436
+ strokeLinecap="round"
437
+ strokeLinejoin="round"
438
+ >
439
+ <path d="M19 12H5M12 19l-7-7 7-7" />
440
+ </svg>
441
+ Adjust Pin
442
+ </button>
443
+ <button
444
+ type="submit"
445
+ className="qr-btn qr-btn--primary qr-btn--grow"
446
+ disabled={state.submitting || !state.fields.flatNumber.trim()}
447
+ >
448
+ {state.submitting ? (
449
+ <>
450
+ <span className="qr-spinner qr-spinner--sm" aria-hidden="true" />
451
+ Saving…
452
+ </>
453
+ ) : (
454
+ <>
455
+ Save Address
456
+ <svg
457
+ aria-hidden="true"
458
+ viewBox="0 0 24 24"
459
+ fill="none"
460
+ stroke="currentColor"
461
+ strokeWidth="2.5"
462
+ strokeLinecap="round"
463
+ strokeLinejoin="round"
464
+ >
465
+ <path d="M20 6L9 17l-5-5" />
466
+ </svg>
467
+ </>
468
+ )}
469
+ </button>
470
+ </div>
471
+ </form>
472
+ )}
473
+ </div>
474
+ );
475
+ };
476
+
477
+ export default AddressForm;
@@ -0,0 +1,218 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ SafeAreaView,
7
+ Linking,
8
+ } from 'react-native';
9
+ // Explicit .native imports so TypeScript (tsconfig.native.json) resolves the
10
+ // correct platform files — Metro handles implicit resolution at bundle time.
11
+ import MapPinSelector from './MapPinSelector.native';
12
+ import AddressForm from './AddressForm.native';
13
+ import type { CheckoutWidgetProps, CompleteAddress } from '../core/types';
14
+ import { styles, COLORS } from '../styles/checkout.native';
15
+
16
+ // ─── Types ────────────────────────────────────────────────────────────────────
17
+
18
+ type Step = 'map' | 'form' | 'done';
19
+
20
+ interface ConfirmedLocation {
21
+ lat: number;
22
+ lng: number;
23
+ digipin: string;
24
+ }
25
+
26
+ // ─── Success screen ───────────────────────────────────────────────────────────
27
+
28
+ interface SuccessScreenProps {
29
+ address: CompleteAddress;
30
+ onEditAddress: () => void;
31
+ isDark: boolean;
32
+ }
33
+
34
+ const SuccessScreen: React.FC<SuccessScreenProps> = ({ address, onEditAddress, isDark }) => (
35
+ <View style={styles.successScreen}>
36
+ <Text style={styles.successIcon}>✅</Text>
37
+
38
+ <Text style={[styles.successTitle, isDark && styles.successTitleDark]}>
39
+ Address Saved!
40
+ </Text>
41
+
42
+ <Text style={[styles.successAddress, isDark && styles.successAddressDark]}>
43
+ {address.formattedAddress}
44
+ </Text>
45
+
46
+ {/* DigiPin badge */}
47
+ <View style={[styles.successDigipinBadge, isDark && styles.successDigipinBadgeDark]}>
48
+ <Text style={styles.digipinLabel}>DigiPin</Text>
49
+ <Text style={styles.digipinCode}>{address.digipin}</Text>
50
+ </View>
51
+
52
+ <TouchableOpacity
53
+ style={[styles.btnGhost, { marginTop: 4 }]}
54
+ onPress={onEditAddress}
55
+ activeOpacity={0.8}
56
+ accessibilityRole="button"
57
+ >
58
+ <Text style={[styles.btnGhostText, isDark && styles.btnGhostTextDark]}>
59
+ Change Address
60
+ </Text>
61
+ </TouchableOpacity>
62
+ </View>
63
+ );
64
+
65
+ // ─── Main Widget ──────────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * QuantaRoute Checkout Widget — native (iOS/Android) version.
69
+ *
70
+ * Two-step address collection:
71
+ * Step 1 – Map: drag pin to exact location (DigiPin computed offline, real-time)
72
+ * Step 2 – Form: auto-filled address + manual flat/building details
73
+ *
74
+ * Metro resolves this file on native; CheckoutWidget.tsx is used on web.
75
+ *
76
+ * Usage in Expo:
77
+ * import { CheckoutWidget } from '@quantaroute/checkout';
78
+ * <CheckoutWidget apiKey="..." onComplete={(addr) => console.log(addr)} />
79
+ */
80
+ const CheckoutWidget: React.FC<CheckoutWidgetProps> = ({
81
+ apiKey,
82
+ apiBaseUrl = 'https://api.quantaroute.com',
83
+ onComplete,
84
+ onError,
85
+ defaultLat,
86
+ defaultLng,
87
+ theme = 'light',
88
+ style,
89
+ mapHeight = 380,
90
+ title = 'Add Delivery Address',
91
+ }) => {
92
+ const [step, setStep] = useState<Step>('map');
93
+ const [confirmedLocation, setConfirmedLocation] = useState<ConfirmedLocation | null>(null);
94
+ const [completedAddress, setCompletedAddress] = useState<CompleteAddress | null>(null);
95
+
96
+ const isDark = theme === 'dark';
97
+
98
+ // ── Handlers ──────────────────────────────────────────────────────────────
99
+
100
+ const handleLocationConfirm = (lat: number, lng: number, digipin: string) => {
101
+ setConfirmedLocation({ lat, lng, digipin });
102
+ setStep('form');
103
+ };
104
+
105
+ const handleAddressComplete = (address: CompleteAddress) => {
106
+ setCompletedAddress(address);
107
+ setStep('done');
108
+ onComplete(address);
109
+ };
110
+
111
+ const handleBack = () => setStep('map');
112
+
113
+ const handleEditAddress = () => {
114
+ setStep('map');
115
+ setConfirmedLocation(null);
116
+ setCompletedAddress(null);
117
+ };
118
+
119
+ // ── Step indicator ─────────────────────────────────────────────────────────
120
+
121
+ const renderStepDots = () => (
122
+ <View style={styles.progress}>
123
+ {/* Dot 1 – always active */}
124
+ <View style={[styles.progressDot, styles.progressDotActive]} />
125
+ {/* Line */}
126
+ <View
127
+ style={[
128
+ styles.progressLine,
129
+ (step === 'form' || step === 'done') && styles.progressLineActive,
130
+ ]}
131
+ />
132
+ {/* Dot 2 */}
133
+ <View
134
+ style={[
135
+ styles.progressDot,
136
+ (step === 'form' || step === 'done') && styles.progressDotActive,
137
+ ]}
138
+ />
139
+ </View>
140
+ );
141
+
142
+ // ── Render ─────────────────────────────────────────────────────────────────
143
+
144
+ return (
145
+ <SafeAreaView
146
+ style={[
147
+ styles.container,
148
+ isDark && styles.containerDark,
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ style as any,
151
+ ]}
152
+ accessibilityLabel="QuantaRoute checkout widget"
153
+ >
154
+ {/* Widget header (hidden on success screen) */}
155
+ {step !== 'done' ? (
156
+ <View style={[styles.header, isDark && styles.headerDark]}>
157
+ <View style={styles.headerBrand}>
158
+ <Text style={[styles.headerIcon, { color: COLORS.primary }]}>📍</Text>
159
+ <Text style={[styles.headerTitle, isDark && styles.headerTitleDark]}>
160
+ {title}
161
+ </Text>
162
+ </View>
163
+ {renderStepDots()}
164
+ </View>
165
+ ) : null}
166
+
167
+ {/* ── Step 1: Map ── */}
168
+ {step === 'map' ? (
169
+ <MapPinSelector
170
+ onLocationConfirm={handleLocationConfirm}
171
+ defaultLat={defaultLat}
172
+ defaultLng={defaultLng}
173
+ mapHeight={mapHeight}
174
+ theme={theme}
175
+ />
176
+ ) : null}
177
+
178
+ {/* ── Step 2: Form ── */}
179
+ {step === 'form' && confirmedLocation ? (
180
+ <AddressForm
181
+ digipin={confirmedLocation.digipin}
182
+ lat={confirmedLocation.lat}
183
+ lng={confirmedLocation.lng}
184
+ apiKey={apiKey}
185
+ apiBaseUrl={apiBaseUrl}
186
+ onAddressComplete={handleAddressComplete}
187
+ onBack={handleBack}
188
+ onError={onError}
189
+ theme={theme}
190
+ />
191
+ ) : null}
192
+
193
+ {/* ── Step 3: Done ── */}
194
+ {step === 'done' && completedAddress ? (
195
+ <SuccessScreen
196
+ address={completedAddress}
197
+ onEditAddress={handleEditAddress}
198
+ isDark={isDark}
199
+ />
200
+ ) : null}
201
+
202
+ {/* Powered-by footer */}
203
+ <View style={[styles.footer, isDark && styles.footerDark]}>
204
+ <Text style={[styles.footerText, isDark && styles.footerTextDark]}>Powered by</Text>
205
+ <TouchableOpacity
206
+ onPress={() => void Linking.openURL('https://quantaroute.com')}
207
+ accessibilityRole="link"
208
+ accessibilityLabel="QuantaRoute website"
209
+ >
210
+ <Text style={styles.footerLink}>QuantaRoute</Text>
211
+ </TouchableOpacity>
212
+ <Text style={[styles.footerText, isDark && styles.footerTextDark]}> 🇮🇳</Text>
213
+ </View>
214
+ </SafeAreaView>
215
+ );
216
+ };
217
+
218
+ export default CheckoutWidget;