@tagadapay/plugin-sdk 1.0.11 → 1.0.13

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 @@
1
+ export declare const AddressFormExample: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAddress } from '../hooks/useAddress';
3
+ // Basic Address Form Example
4
+ export const AddressFormExample = () => {
5
+ const { fields, countries, states, setValue, validateAll, getAddressObject, getFormattedAddress } = useAddress({
6
+ autoValidate: true,
7
+ initialValues: {
8
+ firstName: 'John',
9
+ lastName: 'Doe',
10
+ country: 'US',
11
+ },
12
+ });
13
+ const handleSubmit = (e) => {
14
+ e.preventDefault();
15
+ if (validateAll()) {
16
+ const addressData = getAddressObject();
17
+ console.log('Address submitted:', addressData);
18
+ console.log('Formatted address:', getFormattedAddress());
19
+ }
20
+ else {
21
+ console.log('Form has validation errors');
22
+ }
23
+ };
24
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "mx-auto max-w-md space-y-4 p-6", children: [_jsx("h2", { className: "mb-4 text-xl font-bold", children: "Address Form" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "First Name" }), _jsx("input", { type: "text", value: fields.firstName.value, onChange: (e) => setValue('firstName', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.firstName.error ? 'border-red-500' : 'border-gray-300'}` }), fields.firstName.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.firstName.error })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Last Name" }), _jsx("input", { type: "text", value: fields.lastName.value, onChange: (e) => setValue('lastName', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.lastName.error ? 'border-red-500' : 'border-gray-300'}` }), fields.lastName.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.lastName.error })] })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Email" }), _jsx("input", { type: "email", value: fields.email.value, onChange: (e) => setValue('email', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.email.error ? 'border-red-500' : 'border-gray-300'}` }), fields.email.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.email.error })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Phone" }), _jsx("input", { type: "tel", value: fields.phone.value, onChange: (e) => setValue('phone', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.phone.error ? 'border-red-500' : 'border-gray-300'}` }), fields.phone.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.phone.error })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Address Line 1" }), _jsx("input", { type: "text", value: fields.address1.value, onChange: (e) => setValue('address1', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.address1.error ? 'border-red-500' : 'border-gray-300'}` }), fields.address1.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.address1.error })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Address Line 2 (Optional)" }), _jsx("input", { type: "text", value: fields.address2.value, onChange: (e) => setValue('address2', e.target.value), className: "w-full rounded-md border border-gray-300 px-3 py-2" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "City" }), _jsx("input", { type: "text", value: fields.city.value, onChange: (e) => setValue('city', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.city.error ? 'border-red-500' : 'border-gray-300'}` }), fields.city.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.city.error })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "Postal Code" }), _jsx("input", { type: "text", value: fields.postal.value, onChange: (e) => setValue('postal', e.target.value), className: `w-full rounded-md border px-3 py-2 ${fields.postal.error ? 'border-red-500' : 'border-gray-300'}` }), fields.postal.error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: fields.postal.error })] })] }), _jsxs("div", { className: "rounded-md bg-gray-100 p-4", children: [_jsx("h3", { className: "mb-2 text-sm font-semibold", children: "Form Data:" }), _jsx("pre", { className: "text-xs", children: JSON.stringify(getAddressObject(), null, 2) })] }), _jsxs("div", { className: "text-xs text-gray-500", children: [_jsxs("p", { children: ["Available Countries: ", countries.length] }), _jsxs("p", { children: ["Available States for ", fields.country.value, ": ", states.length] }), _jsxs("p", { children: ["Form Valid: ", validateAll() ? 'Yes' : 'No'] })] }), _jsx("button", { type: "button", onClick: () => {
25
+ if (validateAll()) {
26
+ alert('Form is valid! Data: ' + JSON.stringify(getAddressObject()));
27
+ }
28
+ else {
29
+ alert('Please fix the errors in the form');
30
+ }
31
+ }, className: "w-full rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:opacity-50", children: "Submit" })] }));
32
+ };
@@ -0,0 +1,58 @@
1
+ export interface Country {
2
+ code: string;
3
+ name: string;
4
+ }
5
+ export interface State {
6
+ code: string;
7
+ name: string;
8
+ countryCode: string;
9
+ }
10
+ export interface AddressField {
11
+ value: string;
12
+ isValid: boolean;
13
+ error?: string;
14
+ }
15
+ export interface AddressData {
16
+ firstName: string;
17
+ lastName: string;
18
+ email: string;
19
+ phone: string;
20
+ country: string;
21
+ address1: string;
22
+ address2: string;
23
+ city: string;
24
+ state: string;
25
+ postal: string;
26
+ }
27
+ export interface UseAddressOptions {
28
+ autoValidate?: boolean;
29
+ enableGooglePlaces?: boolean;
30
+ googlePlacesApiKey?: string;
31
+ initialValues?: Partial<AddressData>;
32
+ validation?: Partial<Record<keyof AddressData, (value: string) => string | undefined>>;
33
+ countryRestrictions?: string[];
34
+ onFieldsChange?: (addressData: AddressData) => void;
35
+ debounceConfig?: {
36
+ manualInputDelay?: number;
37
+ googlePlacesDelay?: number;
38
+ enabled?: boolean;
39
+ };
40
+ }
41
+ export interface UseAddressResult {
42
+ fields: Record<keyof AddressData, AddressField>;
43
+ countries: Country[];
44
+ states: State[];
45
+ setValue: (field: keyof AddressData, value: string) => void;
46
+ setValues: (values: Partial<AddressData>) => void;
47
+ resetField: (field: keyof AddressData) => void;
48
+ resetForm: () => void;
49
+ validate: (field: keyof AddressData) => boolean;
50
+ validateAll: () => boolean;
51
+ getFormattedAddress: () => string;
52
+ getAddressObject: () => AddressData;
53
+ addressRef?: React.MutableRefObject<HTMLInputElement | null>;
54
+ addressInputValue?: string;
55
+ handleAddressChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
56
+ isAddressSelected?: boolean;
57
+ }
58
+ export declare function useAddress(options?: UseAddressOptions): UseAddressResult;
@@ -0,0 +1,537 @@
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
+ initialFields[field] = {
75
+ value,
76
+ isValid: !autoValidate || !validationRules[field]?.(value),
77
+ error: autoValidate ? validationRules[field]?.(value) : undefined,
78
+ };
79
+ });
80
+ return initialFields;
81
+ });
82
+ // Google Places integration
83
+ const [isAddressSelected, setIsAddressSelected] = useState(false);
84
+ const [addressInputValue, setAddressInputValue] = useState(fields.address1.value);
85
+ // Transform countries data to our format
86
+ const countries = useMemo(() => {
87
+ let countryList = COUNTRIES_DATA.map((country) => ({
88
+ code: country['alpha-2'],
89
+ name: country.name,
90
+ }));
91
+ // Apply country restrictions if provided
92
+ if (countryRestrictions.length > 0) {
93
+ countryList = countryList.filter((country) => countryRestrictions.includes(country.code));
94
+ }
95
+ return countryList.sort((a, b) => a.name.localeCompare(b.name));
96
+ }, [countryRestrictions]);
97
+ // Transform states data to our format and filter by selected country
98
+ const states = useMemo(() => {
99
+ if (!fields.country.value)
100
+ return [];
101
+ return STATES_DATA.filter((state) => state.country_code === fields.country.value && state.country_code) // Filter out undefined country_code
102
+ .map((state) => ({
103
+ code: state.state_code || '',
104
+ name: state.state_name,
105
+ countryCode: state.country_code, // Use non-null assertion since we filtered above
106
+ }))
107
+ .sort((a, b) => a.name.localeCompare(b.name));
108
+ }, [fields.country.value]);
109
+ // Auto-save state management
110
+ const [autoSaveTimeout, setAutoSaveTimeout] = useState(null);
111
+ const [isGooglePlacesUpdating, setIsGooglePlacesUpdating] = useState(false);
112
+ // Use ref to always access current fields (avoids stale closure issues)
113
+ const fieldsRef = useRef(fields);
114
+ useEffect(() => {
115
+ fieldsRef.current = fields;
116
+ }, [fields]);
117
+ // Smart auto-save trigger with configurable debouncing
118
+ const triggerAutoSave = useCallback((type = 'manual') => {
119
+ if (!onFieldsChange || !debounceEnabled)
120
+ return;
121
+ // Clear existing timeout
122
+ if (autoSaveTimeout) {
123
+ clearTimeout(autoSaveTimeout);
124
+ }
125
+ // Determine delay based on trigger type
126
+ const delay = type === 'googlePlaces' ? googlePlacesDelay : manualInputDelay;
127
+ console.log(`📝 useAddress: Scheduling auto-save (${type}) in ${delay}ms`);
128
+ // Set new timeout with appropriate debounce
129
+ const timeoutId = setTimeout(() => {
130
+ // Get current address data using ref (always fresh, no stale closures)
131
+ const currentFields = fieldsRef.current;
132
+ const addressData = {
133
+ firstName: currentFields.firstName.value,
134
+ lastName: currentFields.lastName.value,
135
+ email: currentFields.email.value,
136
+ phone: currentFields.phone.value,
137
+ country: currentFields.country.value,
138
+ address1: currentFields.address1.value,
139
+ address2: currentFields.address2.value,
140
+ city: currentFields.city.value,
141
+ state: currentFields.state.value,
142
+ postal: currentFields.postal.value,
143
+ };
144
+ console.log(`🔄 useAddress: Auto-save triggered (${type}) with data:`, addressData);
145
+ // Call the auto-save callback directly (no React state setter involved)
146
+ onFieldsChange(addressData);
147
+ setAutoSaveTimeout(null);
148
+ }, delay);
149
+ setAutoSaveTimeout(timeoutId);
150
+ }, [onFieldsChange, autoSaveTimeout, debounceEnabled, manualInputDelay, googlePlacesDelay]);
151
+ // Handle Google Places selection
152
+ const handlePlaceSelected = useCallback((place) => {
153
+ // Clear any pending auto-saves and mark Google Places as updating
154
+ if (autoSaveTimeout) {
155
+ clearTimeout(autoSaveTimeout);
156
+ setAutoSaveTimeout(null);
157
+ }
158
+ setIsGooglePlacesUpdating(true);
159
+ let streetNumber = '';
160
+ let route = '';
161
+ let locality = '';
162
+ let postal_town = '';
163
+ let newCountry = '';
164
+ let stateValue = '';
165
+ console.log('🌍 Google Places selected:', place);
166
+ for (const component of place.address_components || []) {
167
+ const componentType = component.types[0];
168
+ switch (componentType) {
169
+ case 'street_number':
170
+ streetNumber = component.long_name;
171
+ break;
172
+ case 'route':
173
+ route = component.long_name;
174
+ break;
175
+ case 'locality':
176
+ locality = component.long_name;
177
+ break;
178
+ case 'postal_town':
179
+ postal_town = component.long_name;
180
+ if (postal_town !== locality) {
181
+ // Prefer postal_town over locality for city
182
+ locality = postal_town;
183
+ }
184
+ break;
185
+ case 'postal_code':
186
+ break; // We'll handle this separately
187
+ case 'postal_code_prefix':
188
+ break; // We'll handle this separately
189
+ case 'administrative_area_level_1':
190
+ stateValue = component.short_name;
191
+ break;
192
+ case 'country':
193
+ newCountry = component.short_name;
194
+ break;
195
+ }
196
+ }
197
+ // Set address first
198
+ const addressValue = `${streetNumber} ${route}`.trim();
199
+ setAddressInputValue(addressValue);
200
+ // Helper function to update field with validation to clear errors
201
+ const updateFieldWithValidation = (field, value) => {
202
+ const error = validationRules[field]?.(value);
203
+ return {
204
+ value,
205
+ isValid: !error,
206
+ error,
207
+ };
208
+ };
209
+ console.log('🔄 Updating fields from Google Places:', {
210
+ address1: addressValue,
211
+ city: locality,
212
+ country: newCountry,
213
+ stateValue,
214
+ });
215
+ // Use requestAnimationFrame to ensure updates happen in the next frame
216
+ requestAnimationFrame(() => {
217
+ // Update fields with validation to clear any existing errors
218
+ setFields((prev) => ({
219
+ ...prev,
220
+ address1: updateFieldWithValidation('address1', String(addressValue)),
221
+ city: updateFieldWithValidation('city', String(locality)),
222
+ country: updateFieldWithValidation('country', String(newCountry)),
223
+ }));
224
+ // Handle postal code separately with validation
225
+ const postalComponent = place.address_components?.find((comp) => Array.isArray(comp.types) &&
226
+ (comp.types.includes('postal_code') ||
227
+ comp.types.includes('postal_code_prefix')));
228
+ if (postalComponent) {
229
+ setFields((prev) => ({
230
+ ...prev,
231
+ postal: updateFieldWithValidation('postal', String(postalComponent.long_name)),
232
+ }));
233
+ }
234
+ // Handle state after country is set (with small delay to ensure country is processed first)
235
+ if (newCountry && stateValue) {
236
+ setTimeout(() => {
237
+ const mappedStateValue = mapGoogleStateToStateCode(newCountry, stateValue);
238
+ console.log('🗺️ Mapped state value:', mappedStateValue);
239
+ setFields((prev) => ({
240
+ ...prev,
241
+ state: updateFieldWithValidation('state', String(mappedStateValue || stateValue)),
242
+ }));
243
+ console.log('✅ Google Places update completed');
244
+ // Mark updating as complete and trigger single auto-save
245
+ setIsGooglePlacesUpdating(false);
246
+ setTimeout(() => {
247
+ console.log('🌍 useAddress: Google Places completed - triggering auto-save');
248
+ triggerAutoSave('googlePlaces'); // Use Google Places specific timing
249
+ }, 200); // Extra delay to ensure all state updates are complete
250
+ }, 50); // Small delay to ensure country state processing
251
+ }
252
+ else {
253
+ // Mark updating as complete and trigger auto-save (for countries without states)
254
+ setIsGooglePlacesUpdating(false);
255
+ setTimeout(() => {
256
+ console.log('🌍 useAddress: Google Places completed (no state) - triggering auto-save');
257
+ triggerAutoSave('googlePlaces'); // Use Google Places specific timing
258
+ }, 200);
259
+ }
260
+ setIsAddressSelected(true);
261
+ });
262
+ }, [validationRules, triggerAutoSave, autoSaveTimeout]);
263
+ // Memoize Google Places options to prevent re-initialization
264
+ const placesOptions = useMemo(() => ({
265
+ types: ['address'],
266
+ componentRestrictions: countryRestrictions.length > 0 ? { country: countryRestrictions } : undefined,
267
+ }), [countryRestrictions]);
268
+ // Setup Google Places autocomplete
269
+ const { ref: placesRef } = usePlacesWidget({
270
+ apiKey: googlePlacesApiKey || process.env.NEXT_PUBLIC_GOOGLE_AUTOCOMPLETE_API_KEY,
271
+ onPlaceSelected: handlePlaceSelected,
272
+ options: placesOptions,
273
+ });
274
+ // Custom styles for Google Places dropdown
275
+ useEffect(() => {
276
+ if (enableGooglePlaces && !document.getElementById('google-places-custom-styles')) {
277
+ const autocompleteStyles = `
278
+ .pac-container {
279
+ border-radius: 8px;
280
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
281
+ border: 1px solid #e2e8f0;
282
+ margin-top: 4px;
283
+ font-family: inherit;
284
+ z-index: 9999;
285
+ }
286
+ .pac-item {
287
+ padding: 10px 12px;
288
+ font-size: 14px;
289
+ cursor: pointer;
290
+ border-top: 1px solid #f3f4f6;
291
+ }
292
+ .pac-item:first-child { border-top: none; }
293
+ .pac-item:hover { background-color: #f8fafc; }
294
+ .pac-item-selected, .pac-item-selected:hover { background-color: #eef2ff; }
295
+ .pac-matched { font-weight: 600; }
296
+ `;
297
+ const styleElement = document.createElement('style');
298
+ styleElement.id = 'google-places-custom-styles';
299
+ styleElement.textContent = autocompleteStyles;
300
+ document.head.appendChild(styleElement);
301
+ }
302
+ }, [enableGooglePlaces]);
303
+ // Handle immediate form field updates (no debounce for responsive typing)
304
+ const handleAddressChange = useCallback((event) => {
305
+ const value = event.target.value;
306
+ // Update local state immediately for responsive UI
307
+ setAddressInputValue(value);
308
+ // Update form field immediately (no debounce)
309
+ setFields((prev) => ({
310
+ ...prev,
311
+ address1: { ...prev.address1, value },
312
+ }));
313
+ // Reset address selection flag when user types manually
314
+ if (isAddressSelected) {
315
+ setIsAddressSelected(false);
316
+ }
317
+ // ✅ IMPORTANT: Trigger auto-save for manual address typing
318
+ // This ensures that manual typing (without Google Places selection) is saved
319
+ triggerAutoSave('manual');
320
+ }, [isAddressSelected, triggerAutoSave]);
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
+ const error = autoValidate ? validationRules[field]?.(value) : undefined;
342
+ const newFields = {
343
+ ...prev,
344
+ [field]: {
345
+ value,
346
+ isValid: !error,
347
+ error,
348
+ },
349
+ };
350
+ return newFields;
351
+ });
352
+ // Special handling for address field with Google Places
353
+ if (field === 'address1') {
354
+ setAddressInputValue(value);
355
+ }
356
+ // Reset state when country changes
357
+ if (field === 'country') {
358
+ setFields((prev) => ({
359
+ ...prev,
360
+ state: { ...prev.state, value: '', error: undefined, isValid: true },
361
+ }));
362
+ }
363
+ // Trigger auto-save after field update with longer debounce for manual typing
364
+ triggerAutoSave('manual'); // Use manual input timing
365
+ }, [autoValidate, validationRules, triggerAutoSave]);
366
+ const setValues = useCallback((values) => {
367
+ setFields((prev) => {
368
+ const newFields = { ...prev };
369
+ Object.entries(values).forEach(([key, value]) => {
370
+ const field = key;
371
+ const error = autoValidate ? validationRules[field]?.(value || '') : undefined;
372
+ newFields[field] = {
373
+ value: value || '',
374
+ isValid: !error,
375
+ error,
376
+ };
377
+ });
378
+ return newFields;
379
+ });
380
+ }, [autoValidate, validationRules]);
381
+ const resetField = useCallback((field) => {
382
+ setFields((prev) => ({
383
+ ...prev,
384
+ [field]: {
385
+ value: '',
386
+ isValid: true,
387
+ error: undefined,
388
+ },
389
+ }));
390
+ }, []);
391
+ const resetForm = useCallback(() => {
392
+ const resetFields = {};
393
+ const fieldNames = [
394
+ 'firstName',
395
+ 'lastName',
396
+ 'email',
397
+ 'phone',
398
+ 'country',
399
+ 'address1',
400
+ 'address2',
401
+ 'city',
402
+ 'state',
403
+ 'postal',
404
+ ];
405
+ fieldNames.forEach((field) => {
406
+ resetFields[field] = {
407
+ value: '',
408
+ isValid: true,
409
+ error: undefined,
410
+ };
411
+ });
412
+ setFields(resetFields);
413
+ setAddressInputValue('');
414
+ setIsAddressSelected(false);
415
+ }, []);
416
+ // Validate specific field
417
+ const validateField = useCallback((fieldName) => {
418
+ const value = fields[fieldName].value;
419
+ const rule = validationRules[fieldName];
420
+ const error = rule ? rule(value) : undefined;
421
+ setFields((prev) => ({
422
+ ...prev,
423
+ [fieldName]: { ...prev[fieldName], isValid: !error, error },
424
+ }));
425
+ return !error;
426
+ }, [fields, validationRules]);
427
+ // Validate all fields
428
+ const validateAll = useCallback(() => {
429
+ const fieldNames = [
430
+ 'firstName',
431
+ 'lastName',
432
+ 'email',
433
+ 'phone',
434
+ 'country',
435
+ 'address1',
436
+ 'address2',
437
+ 'city',
438
+ 'state',
439
+ 'postal',
440
+ ];
441
+ let isValid = true;
442
+ const newFields = { ...fields };
443
+ fieldNames.forEach((key) => {
444
+ const field = key;
445
+ const value = fields[field].value;
446
+ // Skip state validation for countries without states
447
+ if (field === 'state' &&
448
+ fields.country.value &&
449
+ COUNTRIES_WITHOUT_STATE_FIELD.includes(fields.country.value)) {
450
+ newFields[field] = { ...newFields[field], isValid: true, error: undefined };
451
+ return;
452
+ }
453
+ const error = validationRules[field]?.(value);
454
+ newFields[field] = {
455
+ ...newFields[field],
456
+ isValid: !error,
457
+ error,
458
+ };
459
+ if (error)
460
+ isValid = false;
461
+ });
462
+ setFields(newFields);
463
+ return isValid;
464
+ }, [fields, validationRules]);
465
+ // Check if all required fields are valid
466
+ const isValid = useCallback(() => {
467
+ const { address1, city, state, postal, country } = fields;
468
+ return address1.isValid && city.isValid && state.isValid && postal.isValid && country.isValid;
469
+ }, [fields]);
470
+ // Get form data
471
+ const getFormData = useCallback(() => {
472
+ return {
473
+ firstName: fields.firstName.value,
474
+ lastName: fields.lastName.value,
475
+ email: fields.email.value,
476
+ phone: fields.phone.value,
477
+ country: fields.country.value,
478
+ address1: fields.address1.value,
479
+ address2: fields.address2.value,
480
+ city: fields.city.value,
481
+ state: fields.state.value,
482
+ postal: fields.postal.value,
483
+ };
484
+ }, [fields]);
485
+ const getFormattedAddress = useCallback(() => {
486
+ const { address1, city, state, postal, country } = fields;
487
+ const parts = [
488
+ address1.value,
489
+ city.value,
490
+ state.value,
491
+ postal.value,
492
+ countries.find((c) => c.code === country.value)?.name || country.value,
493
+ ].filter(Boolean);
494
+ return parts.join(', ');
495
+ }, [fields, countries]);
496
+ const getAddressObject = useCallback(() => {
497
+ return {
498
+ firstName: fields.firstName.value,
499
+ lastName: fields.lastName.value,
500
+ email: fields.email.value,
501
+ phone: fields.phone.value,
502
+ country: fields.country.value,
503
+ address1: fields.address1.value,
504
+ address2: fields.address2.value,
505
+ city: fields.city.value,
506
+ state: fields.state.value,
507
+ postal: fields.postal.value,
508
+ };
509
+ }, [fields]);
510
+ // Cleanup auto-save timeout on unmount
511
+ useEffect(() => {
512
+ return () => {
513
+ if (autoSaveTimeout) {
514
+ clearTimeout(autoSaveTimeout);
515
+ }
516
+ };
517
+ }, [autoSaveTimeout]);
518
+ return {
519
+ fields,
520
+ countries,
521
+ states,
522
+ setValue,
523
+ setValues,
524
+ resetField,
525
+ resetForm,
526
+ validate: validateField,
527
+ validateAll,
528
+ getFormattedAddress,
529
+ getAddressObject,
530
+ ...(enableGooglePlaces && {
531
+ addressRef: placesRef,
532
+ addressInputValue,
533
+ handleAddressChange,
534
+ isAddressSelected,
535
+ }),
536
+ };
537
+ }
@@ -3,7 +3,7 @@ import { useTagadaContext } from '../providers/TagadaProvider';
3
3
  import { getCheckoutToken } from '../utils/urlUtils';
4
4
  import { useCurrency } from '../hooks/useCurrency';
5
5
  export function useCheckout(options = {}) {
6
- const { apiService, updateCheckoutDebugData } = useTagadaContext();
6
+ const { apiService, updateCheckoutDebugData, refreshCoordinator } = useTagadaContext();
7
7
  const { code: currentCurrency } = useCurrency();
8
8
  const [checkout, setCheckout] = useState(null);
9
9
  const [isLoading, setIsLoading] = useState(false);
@@ -147,6 +147,13 @@ export function useCheckout(options = {}) {
147
147
  await getCheckout(currentCheckoutTokenRef.current);
148
148
  console.log('✅ [useCheckout] Refresh completed, debug data will be updated automatically');
149
149
  }, [getCheckout]);
150
+ // Register refresh function with coordinator and cleanup on unmount
151
+ useEffect(() => {
152
+ refreshCoordinator.registerCheckoutRefresh(refresh);
153
+ return () => {
154
+ refreshCoordinator.unregisterCheckoutRefresh();
155
+ };
156
+ }, [refresh, refreshCoordinator]);
150
157
  const updateAddress = useCallback(async (data) => {
151
158
  if (!checkout?.checkoutSession.id) {
152
159
  throw new Error('No checkout session available');
@@ -250,6 +257,8 @@ export function useCheckout(options = {}) {
250
257
  });
251
258
  if (response.success) {
252
259
  await refresh();
260
+ // Notify other hooks that checkout data changed
261
+ await refreshCoordinator.notifyCheckoutChanged();
253
262
  }
254
263
  return response;
255
264
  }
@@ -257,7 +266,7 @@ export function useCheckout(options = {}) {
257
266
  const error = err instanceof Error ? err : new Error('Failed to update line items');
258
267
  throw error;
259
268
  }
260
- }, [apiService, checkout?.checkoutSession.id, refresh]);
269
+ }, [apiService, checkout?.checkoutSession.id, refresh, refreshCoordinator]);
261
270
  const addLineItems = useCallback(async (lineItems) => {
262
271
  if (!checkout?.checkoutSession.id) {
263
272
  throw new Error('No checkout session available');