@tagadapay/plugin-sdk 2.1.2 → 2.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/dist/data/iso3166.d.ts +10 -8
- package/dist/data/iso3166.js +79 -16
- package/dist/react/hooks/useCheckout.d.ts +0 -5
- package/dist/react/hooks/useCheckout.js +15 -32
- package/dist/react/hooks/useISOData.d.ts +7 -6
- package/dist/react/hooks/useISOData.js +20 -27
- package/dist/react/hooks/useLogin.js +4 -4
- package/dist/react/hooks/useOffers.js +7 -2
- package/dist/react/hooks/useProducts.js +3 -2
- package/dist/react/index.d.ts +2 -4
- package/dist/react/index.js +1 -3
- package/dist/react/providers/TagadaProvider.d.ts +1 -3
- package/dist/react/providers/TagadaProvider.js +89 -43
- package/dist/react/services/apiService.d.ts +4 -2
- package/dist/react/services/apiService.js +6 -2
- package/package.json +1 -2
- package/dist/react/components/AddressForm.example.d.ts +0 -1
- package/dist/react/components/AddressForm.example.js +0 -32
- package/dist/react/hooks/useAddress.d.ts +0 -59
- package/dist/react/hooks/useAddress.js +0 -557
- package/dist/react/hooks/useAddressV2.d.ts +0 -53
- package/dist/react/hooks/useAddressV2.js +0 -379
|
@@ -1,557 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
|
-
import { usePlacesWidget } from 'react-google-autocomplete';
|
|
3
|
-
// Import the comprehensive countries and states data
|
|
4
|
-
import { countries as COUNTRIES_DATA, states as STATES_DATA } from '../../data/countries';
|
|
5
|
-
// Countries without state fields
|
|
6
|
-
const COUNTRIES_WITHOUT_STATE_FIELD = [
|
|
7
|
-
'VA',
|
|
8
|
-
'MC',
|
|
9
|
-
'SM',
|
|
10
|
-
'LI',
|
|
11
|
-
'LU',
|
|
12
|
-
'MT',
|
|
13
|
-
'SG',
|
|
14
|
-
'IS',
|
|
15
|
-
'EE',
|
|
16
|
-
'LV',
|
|
17
|
-
'LT',
|
|
18
|
-
'SI',
|
|
19
|
-
'SK',
|
|
20
|
-
'ME',
|
|
21
|
-
'AL',
|
|
22
|
-
'XK',
|
|
23
|
-
'MK',
|
|
24
|
-
];
|
|
25
|
-
// Default validation rules
|
|
26
|
-
const defaultValidation = {
|
|
27
|
-
firstName: (value) => (value.trim() ? undefined : 'First name is required'),
|
|
28
|
-
lastName: (value) => (value.trim() ? undefined : 'Last name is required'),
|
|
29
|
-
email: (value) => {
|
|
30
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
31
|
-
if (!value.trim())
|
|
32
|
-
return 'Email is required';
|
|
33
|
-
if (!emailRegex.test(value))
|
|
34
|
-
return 'Valid email is required';
|
|
35
|
-
return undefined;
|
|
36
|
-
},
|
|
37
|
-
phone: (value) => (value.trim() ? undefined : 'Phone number is required'),
|
|
38
|
-
country: (value) => (value.trim() ? undefined : 'Country is required'),
|
|
39
|
-
address1: (value) => (value.trim() ? undefined : 'Address is required'),
|
|
40
|
-
address2: () => undefined, // Optional field
|
|
41
|
-
city: (value) => (value.trim() ? undefined : 'City is required'),
|
|
42
|
-
state: (value) => (value.trim() ? undefined : 'State/Province is required'),
|
|
43
|
-
postal: (value) => (value.trim() ? undefined : 'Zip/Postal code is required'),
|
|
44
|
-
};
|
|
45
|
-
// Map Google Places state names to state codes
|
|
46
|
-
const mapGoogleStateToStateCode = (countryCode, stateName) => {
|
|
47
|
-
const matchingState = STATES_DATA.find((state) => state.country_code === countryCode &&
|
|
48
|
-
(state.state_name === stateName || state.state_code === stateName));
|
|
49
|
-
return matchingState?.state_code || stateName;
|
|
50
|
-
};
|
|
51
|
-
export function useAddress(options = {}) {
|
|
52
|
-
const { autoValidate = false, enableGooglePlaces = false, googlePlacesApiKey, initialValues = {}, validation = {}, countryRestrictions = [], onFieldsChange, debounceConfig = {}, } = options;
|
|
53
|
-
// Extract debounce configuration with defaults
|
|
54
|
-
const { manualInputDelay = 1500, googlePlacesDelay = 300, enabled: debounceEnabled = true, } = debounceConfig;
|
|
55
|
-
// Combine default and custom validation
|
|
56
|
-
const validationRules = { ...defaultValidation, ...validation };
|
|
57
|
-
// Initialize form fields
|
|
58
|
-
const [fields, setFields] = useState(() => {
|
|
59
|
-
const initialFields = {};
|
|
60
|
-
const fieldNames = [
|
|
61
|
-
'firstName',
|
|
62
|
-
'lastName',
|
|
63
|
-
'email',
|
|
64
|
-
'phone',
|
|
65
|
-
'country',
|
|
66
|
-
'address1',
|
|
67
|
-
'address2',
|
|
68
|
-
'city',
|
|
69
|
-
'state',
|
|
70
|
-
'postal',
|
|
71
|
-
];
|
|
72
|
-
fieldNames.forEach((field) => {
|
|
73
|
-
const value = initialValues[field] || '';
|
|
74
|
-
// Don't show errors on initial load, even with autoValidate
|
|
75
|
-
initialFields[field] = {
|
|
76
|
-
value,
|
|
77
|
-
isValid: true, // Start as valid to avoid showing errors on first load
|
|
78
|
-
error: undefined, // No errors on initial load
|
|
79
|
-
touched: false, // Mark as untouched initially
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
return initialFields;
|
|
83
|
-
});
|
|
84
|
-
// Google Places integration
|
|
85
|
-
const [isAddressSelected, setIsAddressSelected] = useState(false);
|
|
86
|
-
const [addressInputValue, setAddressInputValue] = useState(fields.address1.value);
|
|
87
|
-
// Transform countries data to our format
|
|
88
|
-
const countries = useMemo(() => {
|
|
89
|
-
let countryList = COUNTRIES_DATA.map((country) => ({
|
|
90
|
-
code: country['alpha-2'],
|
|
91
|
-
name: country.name,
|
|
92
|
-
}));
|
|
93
|
-
// Apply country restrictions if provided
|
|
94
|
-
if (countryRestrictions.length > 0) {
|
|
95
|
-
countryList = countryList.filter((country) => countryRestrictions.includes(country.code));
|
|
96
|
-
}
|
|
97
|
-
return countryList.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
|
-
}, [countryRestrictions]);
|
|
99
|
-
// Transform states data to our format and filter by selected country
|
|
100
|
-
const states = useMemo(() => {
|
|
101
|
-
if (!fields.country.value)
|
|
102
|
-
return [];
|
|
103
|
-
return STATES_DATA.filter((state) => state.country_code === fields.country.value && state.country_code) // Filter out undefined country_code
|
|
104
|
-
.map((state) => ({
|
|
105
|
-
code: state.state_code || '',
|
|
106
|
-
name: state.state_name,
|
|
107
|
-
countryCode: state.country_code, // Use non-null assertion since we filtered above
|
|
108
|
-
}))
|
|
109
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
110
|
-
}, [fields.country.value]);
|
|
111
|
-
// Auto-save state management
|
|
112
|
-
const [autoSaveTimeout, setAutoSaveTimeout] = useState(null);
|
|
113
|
-
const [isGooglePlacesUpdating, setIsGooglePlacesUpdating] = useState(false);
|
|
114
|
-
// Use ref to always access current fields (avoids stale closure issues)
|
|
115
|
-
const fieldsRef = useRef(fields);
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
fieldsRef.current = fields;
|
|
118
|
-
}, [fields]);
|
|
119
|
-
// Smart auto-save trigger with configurable debouncing
|
|
120
|
-
const triggerAutoSave = useCallback((type = 'manual') => {
|
|
121
|
-
if (!onFieldsChange || !debounceEnabled)
|
|
122
|
-
return;
|
|
123
|
-
// Clear existing timeout
|
|
124
|
-
if (autoSaveTimeout) {
|
|
125
|
-
clearTimeout(autoSaveTimeout);
|
|
126
|
-
}
|
|
127
|
-
// Determine delay based on trigger type
|
|
128
|
-
const delay = type === 'googlePlaces' ? googlePlacesDelay : manualInputDelay;
|
|
129
|
-
console.log(`📝 useAddress: Scheduling auto-save (${type}) in ${delay}ms`);
|
|
130
|
-
// Set new timeout with appropriate debounce
|
|
131
|
-
const timeoutId = setTimeout(() => {
|
|
132
|
-
// Get current address data using ref (always fresh, no stale closures)
|
|
133
|
-
const currentFields = fieldsRef.current;
|
|
134
|
-
const addressData = {
|
|
135
|
-
firstName: currentFields.firstName.value,
|
|
136
|
-
lastName: currentFields.lastName.value,
|
|
137
|
-
email: currentFields.email.value,
|
|
138
|
-
phone: currentFields.phone.value,
|
|
139
|
-
country: currentFields.country.value,
|
|
140
|
-
address1: currentFields.address1.value,
|
|
141
|
-
address2: currentFields.address2.value,
|
|
142
|
-
city: currentFields.city.value,
|
|
143
|
-
state: currentFields.state.value,
|
|
144
|
-
postal: currentFields.postal.value,
|
|
145
|
-
};
|
|
146
|
-
console.log(`🔄 useAddress: Auto-save triggered (${type}) with data:`, addressData);
|
|
147
|
-
// Call the auto-save callback directly (no React state setter involved)
|
|
148
|
-
onFieldsChange(addressData);
|
|
149
|
-
setAutoSaveTimeout(null);
|
|
150
|
-
}, delay);
|
|
151
|
-
setAutoSaveTimeout(timeoutId);
|
|
152
|
-
}, [onFieldsChange, autoSaveTimeout, debounceEnabled, manualInputDelay, googlePlacesDelay]);
|
|
153
|
-
// Handle Google Places selection
|
|
154
|
-
const handlePlaceSelected = useCallback((place) => {
|
|
155
|
-
// Clear any pending auto-saves and mark Google Places as updating
|
|
156
|
-
if (autoSaveTimeout) {
|
|
157
|
-
clearTimeout(autoSaveTimeout);
|
|
158
|
-
setAutoSaveTimeout(null);
|
|
159
|
-
}
|
|
160
|
-
setIsGooglePlacesUpdating(true);
|
|
161
|
-
let streetNumber = '';
|
|
162
|
-
let route = '';
|
|
163
|
-
let locality = '';
|
|
164
|
-
let postal_town = '';
|
|
165
|
-
let newCountry = '';
|
|
166
|
-
let stateValue = '';
|
|
167
|
-
console.log('🌍 Google Places selected:', place);
|
|
168
|
-
for (const component of place.address_components || []) {
|
|
169
|
-
const componentType = component.types[0];
|
|
170
|
-
switch (componentType) {
|
|
171
|
-
case 'street_number':
|
|
172
|
-
streetNumber = component.long_name;
|
|
173
|
-
break;
|
|
174
|
-
case 'route':
|
|
175
|
-
route = component.long_name;
|
|
176
|
-
break;
|
|
177
|
-
case 'locality':
|
|
178
|
-
locality = component.long_name;
|
|
179
|
-
break;
|
|
180
|
-
case 'postal_town':
|
|
181
|
-
postal_town = component.long_name;
|
|
182
|
-
if (postal_town !== locality) {
|
|
183
|
-
// Prefer postal_town over locality for city
|
|
184
|
-
locality = postal_town;
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
case 'postal_code':
|
|
188
|
-
break; // We'll handle this separately
|
|
189
|
-
case 'postal_code_prefix':
|
|
190
|
-
break; // We'll handle this separately
|
|
191
|
-
case 'administrative_area_level_1':
|
|
192
|
-
stateValue = component.short_name;
|
|
193
|
-
break;
|
|
194
|
-
case 'country':
|
|
195
|
-
newCountry = component.short_name;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Set address first
|
|
200
|
-
const addressValue = `${streetNumber} ${route}`.trim();
|
|
201
|
-
setAddressInputValue(addressValue);
|
|
202
|
-
// Helper function to update field with validation to clear errors
|
|
203
|
-
const updateFieldWithValidation = (field, value) => {
|
|
204
|
-
const error = validationRules[field]?.(value);
|
|
205
|
-
return {
|
|
206
|
-
value,
|
|
207
|
-
isValid: !error,
|
|
208
|
-
error,
|
|
209
|
-
touched: true, // Mark as touched when Google Places fills the field
|
|
210
|
-
};
|
|
211
|
-
};
|
|
212
|
-
console.log('🔄 Updating fields from Google Places:', {
|
|
213
|
-
address1: addressValue,
|
|
214
|
-
city: locality,
|
|
215
|
-
country: newCountry,
|
|
216
|
-
stateValue,
|
|
217
|
-
});
|
|
218
|
-
// Use requestAnimationFrame to ensure updates happen in the next frame
|
|
219
|
-
requestAnimationFrame(() => {
|
|
220
|
-
// Update fields with validation to clear any existing errors
|
|
221
|
-
setFields((prev) => ({
|
|
222
|
-
...prev,
|
|
223
|
-
address1: updateFieldWithValidation('address1', String(addressValue)),
|
|
224
|
-
city: updateFieldWithValidation('city', String(locality)),
|
|
225
|
-
country: updateFieldWithValidation('country', String(newCountry)),
|
|
226
|
-
}));
|
|
227
|
-
// Handle postal code separately with validation
|
|
228
|
-
const postalComponent = place.address_components?.find((comp) => Array.isArray(comp.types) &&
|
|
229
|
-
(comp.types.includes('postal_code') ||
|
|
230
|
-
comp.types.includes('postal_code_prefix')));
|
|
231
|
-
if (postalComponent) {
|
|
232
|
-
setFields((prev) => ({
|
|
233
|
-
...prev,
|
|
234
|
-
postal: updateFieldWithValidation('postal', String(postalComponent.long_name)),
|
|
235
|
-
}));
|
|
236
|
-
}
|
|
237
|
-
// Handle state after country is set (with small delay to ensure country is processed first)
|
|
238
|
-
if (newCountry && stateValue) {
|
|
239
|
-
setTimeout(() => {
|
|
240
|
-
const mappedStateValue = mapGoogleStateToStateCode(newCountry, stateValue);
|
|
241
|
-
console.log('🗺️ Mapped state value:', mappedStateValue);
|
|
242
|
-
setFields((prev) => ({
|
|
243
|
-
...prev,
|
|
244
|
-
state: updateFieldWithValidation('state', String(mappedStateValue || stateValue)),
|
|
245
|
-
}));
|
|
246
|
-
console.log('✅ Google Places update completed');
|
|
247
|
-
// Mark updating as complete and trigger single auto-save
|
|
248
|
-
setIsGooglePlacesUpdating(false);
|
|
249
|
-
setTimeout(() => {
|
|
250
|
-
console.log('🌍 useAddress: Google Places completed - triggering auto-save');
|
|
251
|
-
triggerAutoSave('googlePlaces'); // Use Google Places specific timing
|
|
252
|
-
}, 200); // Extra delay to ensure all state updates are complete
|
|
253
|
-
}, 50); // Small delay to ensure country state processing
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
// Mark updating as complete and trigger auto-save (for countries without states)
|
|
257
|
-
setIsGooglePlacesUpdating(false);
|
|
258
|
-
setTimeout(() => {
|
|
259
|
-
console.log('🌍 useAddress: Google Places completed (no state) - triggering auto-save');
|
|
260
|
-
triggerAutoSave('googlePlaces'); // Use Google Places specific timing
|
|
261
|
-
}, 200);
|
|
262
|
-
}
|
|
263
|
-
setIsAddressSelected(true);
|
|
264
|
-
});
|
|
265
|
-
}, [validationRules, triggerAutoSave, autoSaveTimeout]);
|
|
266
|
-
// Memoize Google Places options to prevent re-initialization
|
|
267
|
-
const placesOptions = useMemo(() => ({
|
|
268
|
-
types: ['address'],
|
|
269
|
-
componentRestrictions: countryRestrictions.length > 0 ? { country: countryRestrictions } : undefined,
|
|
270
|
-
}), [countryRestrictions]);
|
|
271
|
-
// Setup Google Places autocomplete
|
|
272
|
-
const { ref: placesRef } = usePlacesWidget({
|
|
273
|
-
apiKey: googlePlacesApiKey || process.env.NEXT_PUBLIC_GOOGLE_AUTOCOMPLETE_API_KEY,
|
|
274
|
-
onPlaceSelected: handlePlaceSelected,
|
|
275
|
-
options: placesOptions,
|
|
276
|
-
});
|
|
277
|
-
// Custom styles for Google Places dropdown
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
if (enableGooglePlaces && !document.getElementById('google-places-custom-styles')) {
|
|
280
|
-
const autocompleteStyles = `
|
|
281
|
-
.pac-container {
|
|
282
|
-
border-radius: 8px;
|
|
283
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
284
|
-
border: 1px solid #e2e8f0;
|
|
285
|
-
margin-top: 4px;
|
|
286
|
-
font-family: inherit;
|
|
287
|
-
z-index: 9999;
|
|
288
|
-
}
|
|
289
|
-
.pac-item {
|
|
290
|
-
padding: 10px 12px;
|
|
291
|
-
font-size: 14px;
|
|
292
|
-
cursor: pointer;
|
|
293
|
-
border-top: 1px solid #f3f4f6;
|
|
294
|
-
}
|
|
295
|
-
.pac-item:first-child { border-top: none; }
|
|
296
|
-
.pac-item:hover { background-color: #f8fafc; }
|
|
297
|
-
.pac-item-selected, .pac-item-selected:hover { background-color: #eef2ff; }
|
|
298
|
-
.pac-matched { font-weight: 600; }
|
|
299
|
-
`;
|
|
300
|
-
const styleElement = document.createElement('style');
|
|
301
|
-
styleElement.id = 'google-places-custom-styles';
|
|
302
|
-
styleElement.textContent = autocompleteStyles;
|
|
303
|
-
document.head.appendChild(styleElement);
|
|
304
|
-
}
|
|
305
|
-
}, [enableGooglePlaces]);
|
|
306
|
-
// Handle immediate form field updates (no debounce for responsive typing)
|
|
307
|
-
const handleAddressChange = useCallback((event) => {
|
|
308
|
-
const value = event.target.value;
|
|
309
|
-
// Update local state immediately for responsive UI
|
|
310
|
-
setAddressInputValue(value);
|
|
311
|
-
// Update form field immediately (no debounce) and mark as touched
|
|
312
|
-
setFields((prev) => ({
|
|
313
|
-
...prev,
|
|
314
|
-
address1: { ...prev.address1, value, touched: true },
|
|
315
|
-
}));
|
|
316
|
-
// Reset address selection flag when user types manually
|
|
317
|
-
if (isAddressSelected) {
|
|
318
|
-
setIsAddressSelected(false);
|
|
319
|
-
}
|
|
320
|
-
}, [isAddressSelected]);
|
|
321
|
-
// Sync addressInputValue with external changes
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
const currentAddress = fields.address1.value;
|
|
324
|
-
if (currentAddress !== addressInputValue) {
|
|
325
|
-
setAddressInputValue(currentAddress);
|
|
326
|
-
}
|
|
327
|
-
}, [fields.address1.value, addressInputValue]);
|
|
328
|
-
// Auto-clear state when country changes to one without states
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
const currentCountry = fields.country.value;
|
|
331
|
-
if (currentCountry && COUNTRIES_WITHOUT_STATE_FIELD.includes(currentCountry)) {
|
|
332
|
-
setFields((prev) => ({
|
|
333
|
-
...prev,
|
|
334
|
-
state: { ...prev.state, value: '' },
|
|
335
|
-
}));
|
|
336
|
-
}
|
|
337
|
-
}, [fields.country.value]);
|
|
338
|
-
// Form methods
|
|
339
|
-
const setValue = useCallback((field, value) => {
|
|
340
|
-
setFields((prev) => {
|
|
341
|
-
// Only show validation errors for touched fields when autoValidate is enabled
|
|
342
|
-
const shouldValidate = autoValidate && prev[field].touched;
|
|
343
|
-
const error = shouldValidate ? validationRules[field]?.(value) : undefined;
|
|
344
|
-
const newFields = {
|
|
345
|
-
...prev,
|
|
346
|
-
[field]: {
|
|
347
|
-
value,
|
|
348
|
-
isValid: !error,
|
|
349
|
-
error,
|
|
350
|
-
touched: true, // Mark field as touched when user interacts with it
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
return newFields;
|
|
354
|
-
});
|
|
355
|
-
// Special handling for address field with Google Places
|
|
356
|
-
if (field === 'address1') {
|
|
357
|
-
setAddressInputValue(value);
|
|
358
|
-
}
|
|
359
|
-
// Reset state when country changes
|
|
360
|
-
if (field === 'country') {
|
|
361
|
-
setFields((prev) => ({
|
|
362
|
-
...prev,
|
|
363
|
-
state: { ...prev.state, value: '', error: undefined, isValid: true, touched: false },
|
|
364
|
-
}));
|
|
365
|
-
}
|
|
366
|
-
// Trigger auto-save after field update with longer debounce for manual typing
|
|
367
|
-
triggerAutoSave('manual'); // Use manual input timing
|
|
368
|
-
}, [autoValidate, validationRules, triggerAutoSave]);
|
|
369
|
-
const setValues = useCallback((values) => {
|
|
370
|
-
setFields((prev) => {
|
|
371
|
-
const newFields = { ...prev };
|
|
372
|
-
Object.entries(values).forEach(([key, value]) => {
|
|
373
|
-
const field = key;
|
|
374
|
-
// Only validate if field was previously touched or if we're setting a non-empty value
|
|
375
|
-
const hasNonEmptyValue = value && value.trim() !== '';
|
|
376
|
-
const shouldValidate = autoValidate && (prev[field].touched || hasNonEmptyValue);
|
|
377
|
-
const error = shouldValidate ? validationRules[field]?.(value || '') : undefined;
|
|
378
|
-
newFields[field] = {
|
|
379
|
-
value: value || '',
|
|
380
|
-
isValid: !error,
|
|
381
|
-
error,
|
|
382
|
-
touched: prev[field].touched || !!hasNonEmptyValue, // Mark as touched if setting a non-empty value
|
|
383
|
-
};
|
|
384
|
-
});
|
|
385
|
-
return newFields;
|
|
386
|
-
});
|
|
387
|
-
}, [autoValidate, validationRules]);
|
|
388
|
-
const resetField = useCallback((field) => {
|
|
389
|
-
setFields((prev) => ({
|
|
390
|
-
...prev,
|
|
391
|
-
[field]: {
|
|
392
|
-
value: '',
|
|
393
|
-
isValid: true,
|
|
394
|
-
error: undefined,
|
|
395
|
-
touched: false,
|
|
396
|
-
},
|
|
397
|
-
}));
|
|
398
|
-
}, []);
|
|
399
|
-
const resetForm = useCallback(() => {
|
|
400
|
-
const resetFields = {};
|
|
401
|
-
const fieldNames = [
|
|
402
|
-
'firstName',
|
|
403
|
-
'lastName',
|
|
404
|
-
'email',
|
|
405
|
-
'phone',
|
|
406
|
-
'country',
|
|
407
|
-
'address1',
|
|
408
|
-
'address2',
|
|
409
|
-
'city',
|
|
410
|
-
'state',
|
|
411
|
-
'postal',
|
|
412
|
-
];
|
|
413
|
-
fieldNames.forEach((field) => {
|
|
414
|
-
resetFields[field] = {
|
|
415
|
-
value: '',
|
|
416
|
-
isValid: true,
|
|
417
|
-
error: undefined,
|
|
418
|
-
touched: false,
|
|
419
|
-
};
|
|
420
|
-
});
|
|
421
|
-
setFields(resetFields);
|
|
422
|
-
setAddressInputValue('');
|
|
423
|
-
setIsAddressSelected(false);
|
|
424
|
-
}, []);
|
|
425
|
-
// Validate specific field
|
|
426
|
-
const validateField = useCallback((fieldName) => {
|
|
427
|
-
const value = fields[fieldName].value;
|
|
428
|
-
const rule = validationRules[fieldName];
|
|
429
|
-
const error = rule ? rule(value) : undefined;
|
|
430
|
-
setFields((prev) => ({
|
|
431
|
-
...prev,
|
|
432
|
-
[fieldName]: {
|
|
433
|
-
...prev[fieldName],
|
|
434
|
-
isValid: !error,
|
|
435
|
-
error,
|
|
436
|
-
touched: true, // Mark as touched when explicitly validating
|
|
437
|
-
},
|
|
438
|
-
}));
|
|
439
|
-
return !error;
|
|
440
|
-
}, [fields, validationRules]);
|
|
441
|
-
// Validate all fields
|
|
442
|
-
const validateAll = useCallback(() => {
|
|
443
|
-
const fieldNames = [
|
|
444
|
-
'firstName',
|
|
445
|
-
'lastName',
|
|
446
|
-
'email',
|
|
447
|
-
'phone',
|
|
448
|
-
'country',
|
|
449
|
-
'address1',
|
|
450
|
-
'address2',
|
|
451
|
-
'city',
|
|
452
|
-
'state',
|
|
453
|
-
'postal',
|
|
454
|
-
];
|
|
455
|
-
let isValid = true;
|
|
456
|
-
const newFields = { ...fields };
|
|
457
|
-
fieldNames.forEach((key) => {
|
|
458
|
-
const field = key;
|
|
459
|
-
const value = fields[field].value;
|
|
460
|
-
// Skip state validation for countries without states
|
|
461
|
-
if (field === 'state' &&
|
|
462
|
-
fields.country.value &&
|
|
463
|
-
COUNTRIES_WITHOUT_STATE_FIELD.includes(fields.country.value)) {
|
|
464
|
-
newFields[field] = {
|
|
465
|
-
...newFields[field],
|
|
466
|
-
isValid: true,
|
|
467
|
-
error: undefined,
|
|
468
|
-
touched: true,
|
|
469
|
-
};
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const error = validationRules[field]?.(value);
|
|
473
|
-
newFields[field] = {
|
|
474
|
-
...newFields[field],
|
|
475
|
-
isValid: !error,
|
|
476
|
-
error,
|
|
477
|
-
touched: true, // Mark all fields as touched when validating all
|
|
478
|
-
};
|
|
479
|
-
if (error)
|
|
480
|
-
isValid = false;
|
|
481
|
-
});
|
|
482
|
-
setFields(newFields);
|
|
483
|
-
return isValid;
|
|
484
|
-
}, [fields, validationRules]);
|
|
485
|
-
// Check if all required fields are valid
|
|
486
|
-
const isValid = useCallback(() => {
|
|
487
|
-
const { address1, city, state, postal, country } = fields;
|
|
488
|
-
return address1.isValid && city.isValid && state.isValid && postal.isValid && country.isValid;
|
|
489
|
-
}, [fields]);
|
|
490
|
-
// Get form data
|
|
491
|
-
const getFormData = useCallback(() => {
|
|
492
|
-
return {
|
|
493
|
-
firstName: fields.firstName.value,
|
|
494
|
-
lastName: fields.lastName.value,
|
|
495
|
-
email: fields.email.value,
|
|
496
|
-
phone: fields.phone.value,
|
|
497
|
-
country: fields.country.value,
|
|
498
|
-
address1: fields.address1.value,
|
|
499
|
-
address2: fields.address2.value,
|
|
500
|
-
city: fields.city.value,
|
|
501
|
-
state: fields.state.value,
|
|
502
|
-
postal: fields.postal.value,
|
|
503
|
-
};
|
|
504
|
-
}, [fields]);
|
|
505
|
-
const getFormattedAddress = useCallback(() => {
|
|
506
|
-
const { address1, city, state, postal, country } = fields;
|
|
507
|
-
const parts = [
|
|
508
|
-
address1.value,
|
|
509
|
-
city.value,
|
|
510
|
-
state.value,
|
|
511
|
-
postal.value,
|
|
512
|
-
countries.find((c) => c.code === country.value)?.name || country.value,
|
|
513
|
-
].filter(Boolean);
|
|
514
|
-
return parts.join(', ');
|
|
515
|
-
}, [fields, countries]);
|
|
516
|
-
const getAddressObject = useCallback(() => {
|
|
517
|
-
return {
|
|
518
|
-
firstName: fields.firstName.value,
|
|
519
|
-
lastName: fields.lastName.value,
|
|
520
|
-
email: fields.email.value,
|
|
521
|
-
phone: fields.phone.value,
|
|
522
|
-
country: fields.country.value,
|
|
523
|
-
address1: fields.address1.value,
|
|
524
|
-
address2: fields.address2.value,
|
|
525
|
-
city: fields.city.value,
|
|
526
|
-
state: fields.state.value,
|
|
527
|
-
postal: fields.postal.value,
|
|
528
|
-
};
|
|
529
|
-
}, [fields]);
|
|
530
|
-
// Cleanup auto-save timeout on unmount
|
|
531
|
-
useEffect(() => {
|
|
532
|
-
return () => {
|
|
533
|
-
if (autoSaveTimeout) {
|
|
534
|
-
clearTimeout(autoSaveTimeout);
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
}, [autoSaveTimeout]);
|
|
538
|
-
return {
|
|
539
|
-
fields,
|
|
540
|
-
countries,
|
|
541
|
-
states,
|
|
542
|
-
setValue,
|
|
543
|
-
setValues,
|
|
544
|
-
resetField,
|
|
545
|
-
resetForm,
|
|
546
|
-
validate: validateField,
|
|
547
|
-
validateAll,
|
|
548
|
-
getFormattedAddress,
|
|
549
|
-
getAddressObject,
|
|
550
|
-
...(enableGooglePlaces && {
|
|
551
|
-
addressRef: placesRef,
|
|
552
|
-
addressInputValue,
|
|
553
|
-
handleAddressChange,
|
|
554
|
-
isAddressSelected,
|
|
555
|
-
}),
|
|
556
|
-
};
|
|
557
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { type Country as ISO3166Country, type State as ISO3166State } from '../../data/iso3166';
|
|
2
|
-
export type Country = ISO3166Country;
|
|
3
|
-
export type State = ISO3166State;
|
|
4
|
-
export interface AddressField {
|
|
5
|
-
value: string;
|
|
6
|
-
isValid: boolean;
|
|
7
|
-
error?: string;
|
|
8
|
-
touched?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export interface AddressData {
|
|
11
|
-
firstName: string;
|
|
12
|
-
lastName: string;
|
|
13
|
-
email: string;
|
|
14
|
-
phone: string;
|
|
15
|
-
country: string;
|
|
16
|
-
address1: string;
|
|
17
|
-
address2: string;
|
|
18
|
-
city: string;
|
|
19
|
-
state: string;
|
|
20
|
-
postal: string;
|
|
21
|
-
}
|
|
22
|
-
export interface UseAddressV2Config {
|
|
23
|
-
autoValidate?: boolean;
|
|
24
|
-
enableGooglePlaces?: boolean;
|
|
25
|
-
googlePlacesApiKey?: string;
|
|
26
|
-
countryRestrictions?: string[];
|
|
27
|
-
onFieldsChange?: (data: AddressData) => void;
|
|
28
|
-
debounceConfig?: {
|
|
29
|
-
autoSaveDelay?: number;
|
|
30
|
-
enabled?: boolean;
|
|
31
|
-
};
|
|
32
|
-
initialValues?: Partial<AddressData>;
|
|
33
|
-
}
|
|
34
|
-
export interface UseAddressV2Return {
|
|
35
|
-
fields: Record<keyof AddressData, AddressField>;
|
|
36
|
-
setValue: (field: keyof AddressData, value: string) => void;
|
|
37
|
-
setValues: (values: Partial<AddressData>) => void;
|
|
38
|
-
getValue: (field: keyof AddressData) => string;
|
|
39
|
-
getValues: () => AddressData;
|
|
40
|
-
validateField: (field: keyof AddressData) => boolean;
|
|
41
|
-
validateAll: () => boolean;
|
|
42
|
-
isValid: boolean;
|
|
43
|
-
reset: () => void;
|
|
44
|
-
countries: Country[];
|
|
45
|
-
states: State[];
|
|
46
|
-
getStatesForCountry: (countryCode: string) => State[];
|
|
47
|
-
addressRef: React.RefObject<HTMLInputElement | null>;
|
|
48
|
-
addressInputValue: string;
|
|
49
|
-
setAddressInputValue: (value: string) => void;
|
|
50
|
-
handleFieldChange: (field: keyof AddressData) => (value: string) => void;
|
|
51
|
-
handleFieldBlur: (field: keyof AddressData) => () => void;
|
|
52
|
-
}
|
|
53
|
-
export declare const useAddressV2: (config?: UseAddressV2Config) => UseAddressV2Return;
|