@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.
@@ -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-sessions/${checkoutSessionId}/pay`, {
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);