@tagadapay/plugin-sdk 1.0.12 → 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.
- package/dist/data/countries.d.ts +50 -0
- package/dist/data/countries.js +38181 -0
- package/dist/react/components/AddressForm.example.d.ts +1 -0
- package/dist/react/components/AddressForm.example.js +32 -0
- package/dist/react/hooks/useAddress.d.ts +58 -0
- package/dist/react/hooks/useAddress.js +537 -0
- package/dist/react/hooks/usePayment.d.ts +2 -0
- package/dist/react/hooks/usePayment.js +7 -4
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +1 -0
- package/package.json +2 -1
|
@@ -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
|
+
}
|
|
@@ -31,6 +31,8 @@ export interface PaymentResponse {
|
|
|
31
31
|
export interface PaymentOptions {
|
|
32
32
|
enableThreeds?: boolean;
|
|
33
33
|
threedsProvider?: ThreedsProvider;
|
|
34
|
+
initiatedBy?: 'customer' | 'merchant';
|
|
35
|
+
source?: 'upsell' | 'checkout' | 'offer' | 'missing_club' | 'forced';
|
|
34
36
|
onSuccess?: (payment: Payment) => void;
|
|
35
37
|
onFailure?: (error: string) => void;
|
|
36
38
|
onRequireAction?: (payment: Payment) => void;
|
|
@@ -204,15 +204,18 @@ export function usePayment() {
|
|
|
204
204
|
throw error;
|
|
205
205
|
}
|
|
206
206
|
}, [basisTheory, apiService]);
|
|
207
|
-
// Process payment directly with checkout session
|
|
207
|
+
// Process payment directly with checkout session (V2)
|
|
208
208
|
const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
|
|
209
209
|
try {
|
|
210
|
-
// Create order and process payment in one call
|
|
211
|
-
const response = await apiService.fetch(`/api/v1/checkout
|
|
210
|
+
// Create order and process payment in one call using V2 endpoint
|
|
211
|
+
const response = await apiService.fetch(`/api/public/v1/checkout/pay-v2`, {
|
|
212
212
|
method: 'POST',
|
|
213
213
|
body: {
|
|
214
|
+
checkoutSessionId,
|
|
214
215
|
paymentInstrumentId,
|
|
215
|
-
threedsSessionId,
|
|
216
|
+
...(threedsSessionId && { threedsSessionId }),
|
|
217
|
+
...(options.initiatedBy && { initiatedBy: options.initiatedBy }),
|
|
218
|
+
...(options.source && { source: options.source }),
|
|
216
219
|
},
|
|
217
220
|
});
|
|
218
221
|
console.log('Payment response:', response);
|