import React, { useEffect, useReducer, useCallback } from 'react'; import { lookupLocation, reverseGeocode, validatePincode, pincodeHasDeliveryOffice, type AddressComponents, } from '../core/api'; import { checkServiceability, type ServiceabilityResult } from '../core/checkout-api'; import { processLocalityLookup } from '../core/locality'; import type { AddressFormProps, AddressLabel, AdministrativeInfo, CompleteAddress, LocationAlternative, } from '../core/types'; import { AddressLabelIcon } from './AddressLabelIcons'; // ─── State ──────────────────────────────────────────────────────────────────── interface ManualFields { flatNumber: string; floorNumber: string; buildingName: string; streetName: string; } type FormState = | { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; adminInfo: AdministrativeInfo; alternatives: LocationAlternative[]; localityOptions: string[]; selectedLocality: string; // Selected locality name (from adminInfo or alternatives) selectedLabel: AddressLabel; labelText: string; fields: ManualFields; submitting: boolean; /** At least one India Post office with "Delivery" (not only "Non Delivery") */ pincodeHasDelivery: boolean; serviceabilityResult: ServiceabilityResult | null; }; type FormAction = | { type: 'LOAD_START' } | { type: 'LOAD_SUCCESS'; adminInfo: AdministrativeInfo; alternatives: LocationAlternative[]; addressComponents: AddressComponents; pincodeHasDelivery: boolean; serviceabilityResult: ServiceabilityResult | null; initialValues?: import('../core/types').AddressFormInitialValues; } | { type: 'LOAD_ERROR'; message: string } | { type: 'SET_FIELD'; key: keyof ManualFields; value: string } | { type: 'SET_LOCALITY'; locality: string } | { type: 'SET_LABEL'; label: AddressLabel } | { type: 'SET_LABEL_TEXT'; value: string } | { type: 'SUBMIT_START' } | { type: 'SUBMIT_END' }; const INITIAL_FIELDS: ManualFields = { flatNumber: '', floorNumber: '', buildingName: '', streetName: '', }; /** * Parse addressComponents from Nominatim/OpenStreetMap. * - name → Building/Society/POI name * - road + suburb → Street/Area */ function parseAddressComponents(components: AddressComponents): Partial { const fields: Partial = {}; // Building/Society/POI name: prefer 'name' field (most relevant) if (components.name) { fields.buildingName = components.name; } else if (components.building_name) { fields.buildingName = components.building_name; } else if (components.addr_housename) { fields.buildingName = components.addr_housename; } // Street/Area: combine road + suburb (if available) const streetParts: string[] = []; if (components.road) { streetParts.push(components.road); } if (components.suburb) { streetParts.push(components.suburb); } if (streetParts.length > 0) { fields.streetName = streetParts.join(', '); } return fields; } function reducer(state: FormState, action: FormAction): FormState { switch (action.type) { case 'LOAD_START': return { status: 'loading' }; case 'LOAD_SUCCESS': { // Pre-fill fields from addressComponents, then override with initialValues const preFilled = parseAddressComponents(action.addressComponents); const iv = action.initialValues; const fields: ManualFields = { ...INITIAL_FIELDS, ...preFilled, ...(iv?.flatNumber !== undefined ? { flatNumber: iv.flatNumber } : {}), ...(iv?.floorNumber !== undefined ? { floorNumber: iv.floorNumber } : {}), ...(iv?.buildingName !== undefined ? { buildingName: iv.buildingName } : {}), ...(iv?.streetName !== undefined ? { streetName: iv.streetName } : {}), }; const processed = processLocalityLookup(action.adminInfo, action.alternatives || []); let localityOptions = processed.localityOptions; let selectedLocality = processed.selectedLocality; if (iv?.locality?.trim()) { const savedLocality = iv.locality.trim(); if (!localityOptions.includes(savedLocality)) { localityOptions = [savedLocality, ...localityOptions]; } selectedLocality = savedLocality; } const selectedLabel = iv?.label ?? 'home'; return { status: 'ready', adminInfo: processed.adminInfo, alternatives: processed.alternatives, localityOptions, selectedLocality, selectedLabel, labelText: selectedLabel === 'other' ? (iv?.labelText ?? '') : '', fields, submitting: false, pincodeHasDelivery: action.pincodeHasDelivery, serviceabilityResult: action.serviceabilityResult, }; } case 'LOAD_ERROR': return { status: 'error', message: action.message }; case 'SET_FIELD': if (state.status !== 'ready') return state; return { ...state, fields: { ...state.fields, [action.key]: action.value } }; case 'SET_LOCALITY': if (state.status !== 'ready') return state; return { ...state, selectedLocality: action.locality }; case 'SET_LABEL': if (state.status !== 'ready') return state; return { ...state, selectedLabel: action.label, labelText: action.label === 'other' ? state.labelText : '', }; case 'SET_LABEL_TEXT': if (state.status !== 'ready') return state; return { ...state, labelText: action.value.slice(0, 64) }; case 'SUBMIT_START': if (state.status !== 'ready') return state; return { ...state, submitting: true }; case 'SUBMIT_END': if (state.status !== 'ready') return state; return { ...state, submitting: false }; default: return state; } } // ─── Component ──────────────────────────────────────────────────────────────── const AddressForm: React.FC = ({ digipin, lat, lng, apiKey, apiBaseUrl, onAddressComplete, onBack, onError, checkoutConfig, serviceabilitySessionToken, enableServiceabilityCheck = false, showIndiaPostDeliveryWarning = false, serviceabilityBlockCheckout, onServiceabilityResult, initialValues, submitLabel = 'Save Address', }) => { const [state, dispatch] = useReducer(reducer, { status: 'loading' }); // ── Fetch administrative data + address components on mount ──────────────── const fetchAdminInfo = useCallback(async () => { dispatch({ type: 'LOAD_START' }); try { // Call both APIs in parallel: // 1. /v1/location/lookup → administrative_info (state, district, pincode) + alternatives // 2. /v1/digipin/reverse → addressComponents (name, road, suburb from Nominatim) const [adminRes, reverseRes] = await Promise.all([ lookupLocation(lat, lng, apiKey, apiBaseUrl), reverseGeocode(digipin, apiKey, apiBaseUrl), ]); const pincode = adminRes.data.administrative_info.pincode?.replace(/\D/g, '').slice(0, 6) ?? ''; if (pincode.length !== 6) { dispatch({ type: 'LOAD_ERROR', message: 'Could not determine a valid 6-digit pincode for this location. Please adjust the map pin.', }); onError?.(new Error('Missing or invalid pincode')); return; } const pinValidation = await validatePincode(pincode, apiKey, apiBaseUrl); if (!pinValidation.isValid) { dispatch({ type: 'LOAD_ERROR', message: 'This location resolves to a pincode that is not in the India Post database. Please move the map pin to a valid address.', }); onError?.(new Error('Invalid pincode for location')); return; } const hasDelivery = pincodeHasDeliveryOffice(pinValidation.offices); let serviceabilityResult: ServiceabilityResult | null = null; if ( enableServiceabilityCheck && checkoutConfig?.functionBaseUrl && checkoutConfig.merchantId && serviceabilitySessionToken ) { try { serviceabilityResult = await checkServiceability( checkoutConfig, serviceabilitySessionToken, pincode, { lat, lng } ); onServiceabilityResult?.(serviceabilityResult); } catch { // Fail open: merchant serviceability should never block address entry // when the checkout-addresses function itself is unreachable. } } dispatch({ type: 'LOAD_SUCCESS', adminInfo: adminRes.data.administrative_info, alternatives: adminRes.data.alternatives || [], addressComponents: reverseRes.data.addressComponents, pincodeHasDelivery: hasDelivery, serviceabilityResult, initialValues, }); } catch (err) { const msg = err instanceof Error ? err.message : 'Failed to fetch address data.'; dispatch({ type: 'LOAD_ERROR', message: msg }); onError?.(err instanceof Error ? err : new Error(msg)); } }, [ digipin, lat, lng, apiKey, apiBaseUrl, onError, checkoutConfig, serviceabilitySessionToken, enableServiceabilityCheck, onServiceabilityResult, initialValues, ]); useEffect(() => { void fetchAdminInfo(); }, [fetchAdminInfo]); // ── Submit ──────────────────────────────────────────────────────────────── const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); if (state.status !== 'ready') return; const { adminInfo, selectedLocality, selectedLabel, labelText, fields } = state; if (selectedLabel === 'other' && !labelText.trim()) return; const serviceabilityBlocked = state.serviceabilityResult?.checked && state.serviceabilityResult.serviceable === false && (serviceabilityBlockCheckout ?? state.serviceabilityResult.block_checkout); if (serviceabilityBlocked) return; const parts: string[] = [ fields.flatNumber, fields.floorNumber ? `Floor ${fields.floorNumber}` : '', fields.buildingName, fields.streetName, selectedLocality, // Use selected locality (may be from alternatives) adminInfo.district, adminInfo.state, adminInfo.pincode, ].filter(Boolean); const complete: CompleteAddress = { digipin, lat, lng, state: adminInfo.state, district: adminInfo.district, division: adminInfo.division, locality: selectedLocality, // Use selected locality pincode: adminInfo.pincode, delivery: adminInfo.delivery, country: adminInfo.country ?? 'India', ...fields, formattedAddress: parts.join(', '), label: selectedLabel, ...(selectedLabel === 'other' ? { labelText: labelText.trim() } : {}), }; dispatch({ type: 'SUBMIT_START' }); try { onAddressComplete(complete); } finally { dispatch({ type: 'SUBMIT_END' }); } }, [state, digipin, lat, lng, onAddressComplete, serviceabilityBlockCheckout] ); const serviceabilityBlocked = state.status === 'ready' && state.serviceabilityResult?.checked && state.serviceabilityResult.serviceable === false && (serviceabilityBlockCheckout ?? state.serviceabilityResult.block_checkout); const showMerchantServiceability = state.status === 'ready' && state.serviceabilityResult?.checked && state.serviceabilityResult.serviceable === false; const showIndiaPostInfo = state.status === 'ready' && !state.pincodeHasDelivery && state.serviceabilityResult?.serviceable !== true && !showMerchantServiceability && (state.serviceabilityResult?.show_india_post_delivery_warning ?? showIndiaPostDeliveryWarning); return (
{/* ── Step header ── */}
2
Add Address Details Flat number and building info
{/* ── DigiPin reference strip ── */}
DigiPin {digipin}
{/* ── Loading skeleton ── */} {state.status === 'loading' && (
)} {/* ── Error state ── */} {state.status === 'error' && (

{state.message}

)} {/* ── Main form ── */} {state.status === 'ready' && (
{/* Auto-filled section */}
Auto-detected from your digipin
{/* State */}
State {state.adminInfo.state}
{/* District */}
District {state.adminInfo.district}
{/* Locality - show dropdown if multiple unique localities exist */}
Locality {state.localityOptions.length > 1 ? ( ) : ( {state.selectedLocality} )}
{/* Pincode */}
Pincode {state.adminInfo.pincode}
{showMerchantServiceability && state.serviceabilityResult && (
{serviceabilityBlocked ? 'Delivery unavailable' : 'Delivery may be limited'} {state.serviceabilityResult.message ?? (serviceabilityBlocked ? 'This seller does not currently deliver to this pincode.' : 'This seller recommends confirming delivery availability for this pincode.')}
)} {showIndiaPostInfo && (
India Post: no home-delivery office Courier partners may still deliver to this pincode.
)} {/* Address label */}
Save as
{( [ { value: 'home' as const, title: 'Home' }, { value: 'work' as const, title: 'Work' }, { value: 'other' as const, title: 'Other' }, ] as const ).map(({ value, title }) => ( ))}
{state.selectedLabel === 'other' && (
dispatch({ type: 'SET_LABEL_TEXT', value: e.target.value }) } maxLength={64} required aria-required="true" autoComplete="off" />
)}
{/* Manual entry section */}
Your details
{/* Flat / House – required */}
dispatch({ type: 'SET_FIELD', key: 'flatNumber', value: e.target.value }) } autoComplete="address-line1" required aria-required="true" />
{/* Floor */}
dispatch({ type: 'SET_FIELD', key: 'floorNumber', value: e.target.value }) } />
{/* Building / Society */}
dispatch({ type: 'SET_FIELD', key: 'buildingName', value: e.target.value }) } autoComplete="address-line2" />
{/* Street / Area */}
dispatch({ type: 'SET_FIELD', key: 'streetName', value: e.target.value }) } autoComplete="address-level3" />
{/* Actions */}
)}
); }; export default AddressForm;