import React, { useEffect, useReducer, useCallback, useState } from 'react'; import { View, Text, TextInput, TouchableOpacity, ScrollView, Modal, FlatList, ActivityIndicator, KeyboardAvoidingView, Platform, } from 'react-native'; 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 { styles, COLORS } from '../styles/checkout.native'; import { AddressDetailsIcon, AddressLabelIcon } from './AddressLabelIcons.native'; // ─── State (identical logic to web AddressForm) ─────────────────────────────── 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; selectedLabel: AddressLabel; labelText: string; fields: ManualFields; submitting: boolean; pincodeHasDelivery: boolean; serviceabilityResult: ServiceabilityResult | null; }; type FormAction = | { type: 'LOAD_START' } | { type: 'LOAD_SUCCESS'; adminInfo: AdministrativeInfo; alternatives: LocationAlternative[]; addressComponents: AddressComponents; pincodeHasDelivery: boolean; serviceabilityResult: ServiceabilityResult | null; } | { 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: '', }; function parseAddressComponents(components: AddressComponents): Partial { const fields: Partial = {}; 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; } 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': { const preFilled = parseAddressComponents(action.addressComponents); const processed = processLocalityLookup(action.adminInfo, action.alternatives || []); return { status: 'ready', adminInfo: processed.adminInfo, alternatives: processed.alternatives, localityOptions: processed.localityOptions, selectedLocality: processed.selectedLocality, selectedLabel: 'home', labelText: '', fields: { ...INITIAL_FIELDS, ...preFilled }, 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 ──────────────────────────────────────────────────────────────── /** * Native (iOS/Android) address form — identical business logic to web AddressForm.tsx, * rewritten with React Native primitives (TextInput, ScrollView, Modal picker). * Metro resolves this file on native; AddressForm.tsx is used on web. */ const AddressForm: React.FC = ({ digipin, lat, lng, apiKey, apiBaseUrl, onAddressComplete, onBack, onError, theme = 'light', checkoutConfig, serviceabilitySessionToken, enableServiceabilityCheck = false, showIndiaPostDeliveryWarning = false, serviceabilityBlockCheckout, onServiceabilityResult, }) => { const [state, dispatch] = useReducer(reducer, { status: 'loading' }); const [showLocalityPicker, setShowLocalityPicker] = useState(false); const [focusedField, setFocusedField] = useState(null); const isDark = theme === 'dark'; // ── Fetch administrative data ────────────────────────────────────────────── const fetchAdminInfo = useCallback(async () => { dispatch({ type: 'LOAD_START' }); try { 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 if the platform serviceability check itself is unavailable. } } dispatch({ type: 'LOAD_SUCCESS', adminInfo: adminRes.data.administrative_info, alternatives: adminRes.data.alternatives || [], addressComponents: reverseRes.data.addressComponents, pincodeHasDelivery: hasDelivery, serviceabilityResult, }); } 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, ]); useEffect(() => { void fetchAdminInfo(); }, [fetchAdminInfo]); // ── Submit ──────────────────────────────────────────────────────────────── const handleSubmit = useCallback(() => { if (state.status !== 'ready') return; if (!state.fields.flatNumber.trim()) return; if (state.selectedLabel === 'other' && !state.labelText.trim()) return; const serviceabilityBlocked = state.serviceabilityResult?.checked && state.serviceabilityResult.serviceable === false && (serviceabilityBlockCheckout ?? state.serviceabilityResult.block_checkout); if (serviceabilityBlocked) return; const { adminInfo, selectedLocality, selectedLabel, labelText, fields } = state; const parts: string[] = [ fields.flatNumber, fields.floorNumber ? `Floor ${fields.floorNumber}` : '', fields.buildingName, fields.streetName, selectedLocality, 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, 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]); // ─── Field input helper ──────────────────────────────────────────────────── const renderField = ( key: keyof ManualFields, label: string, placeholder: string, required = false ) => ( {label} {required ? ( * ) : ( optional )} state.status === 'ready' && dispatch({ type: 'SET_FIELD', key, value }) } onFocus={() => setFocusedField(key)} onBlur={() => setFocusedField(null)} autoCapitalize="words" returnKeyType="next" /> ); 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 state ── */} {state.status === 'loading' ? ( Fetching address details… ) : null} {/* ── Error state ── */} {state.status === 'error' ? ( {state.message} void fetchAdminInfo()} activeOpacity={0.8} > Retry ) : null} {/* ── Main form ── */} {state.status === 'ready' ? ( {/* Auto-detected section */} 📍 Auto-detected from your digipin {/* State & District in a row */} State {state.adminInfo.state} District {state.adminInfo.district} {/* Locality */} Locality {state.localityOptions.length > 1 ? ( setShowLocalityPicker(true)} activeOpacity={0.8} accessibilityRole="button" accessibilityLabel="Select locality" > {state.selectedLocality} ) : ( {state.selectedLocality} )} {/* Pincode */} Pincode {state.adminInfo.pincode} {showMerchantServiceability && state.serviceabilityResult ? ( {serviceabilityBlocked ? '!' : '⚠'} {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.')} ) : null} {showIndiaPostInfo ? ( i India Post: no home-delivery office Courier partners may still deliver to this pincode. ) : null} {/* 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 }) => { const active = state.selectedLabel === value; const iconColor = active ? COLORS.primary : COLORS.textMuted; return ( dispatch({ type: 'SET_LABEL', label: value })} accessibilityRole="radio" accessibilityState={{ checked: active }} accessibilityLabel={title} activeOpacity={0.97} > {title} ); })} {state.selectedLabel === 'other' ? ( Label name * dispatch({ type: 'SET_LABEL_TEXT', value }) } maxLength={64} autoCapitalize="words" returnKeyType="next" /> ) : null} {/* Manual entry section */} Your details {/* Flat / House – required */} {renderField( 'flatNumber', 'Flat / House Number', 'e.g. 4B, Flat 201, House No. 12', true )} {/* Floor + Building in a row */} {renderField('floorNumber', 'Floor', 'e.g. 3rd, Ground')} {renderField('buildingName', 'Building / Society', 'e.g. Sunshine Apts')} {/* Street – full width */} {renderField( 'streetName', 'Street / Area', 'e.g. MG Road, Sector 12' )} ) : null} {/* ── Sticky form actions ── */} {state.status === 'ready' ? ( ← Adjust Pin {state.submitting ? ( ) : ( Save Address ✓ )} ) : null} {/* ── Locality picker modal ── */} {state.status === 'ready' && state.localityOptions.length > 1 ? ( setShowLocalityPicker(false)} accessibilityViewIsModal > setShowLocalityPicker(false)} > Select Locality ({ name }))} keyExtractor={(item) => item.name} renderItem={({ item }) => { const isSelected = item.name === state.selectedLocality; return ( { if (state.status === 'ready') { dispatch({ type: 'SET_LOCALITY', locality: item.name }); } setShowLocalityPicker(false); }} activeOpacity={0.7} > {item.name} {isSelected ? ( ) : null} ); }} /> ) : null} ); }; export default AddressForm;