@tagadapay/plugin-sdk 2.6.10 → 2.6.12
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/react/hooks/useExpressPayment.d.ts +71 -0
- package/dist/react/hooks/useExpressPayment.js +158 -0
- package/dist/v2/core/resources/funnel.d.ts +109 -0
- package/dist/v2/core/resources/funnel.js +38 -0
- package/dist/v2/core/resources/index.d.ts +1 -0
- package/dist/v2/core/resources/index.js +1 -0
- package/dist/v2/core/resources/storeConfig.d.ts +2 -0
- package/dist/v2/index.d.ts +3 -1
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/hooks/useDiscountQuery.d.ts +79 -0
- package/dist/v2/react/hooks/useDiscountQuery.js +24 -0
- package/dist/v2/react/hooks/useFunnel.d.ts +51 -0
- package/dist/v2/react/hooks/useFunnel.js +432 -0
- package/dist/v2/react/hooks/useOrderQuery.js +1 -1
- package/dist/v2/react/hooks/usePostPurchasesQuery.d.ts +1 -1
- package/dist/v2/react/hooks/usePostPurchasesQuery.js +2 -2
- package/dist/v2/react/index.d.ts +4 -0
- package/dist/v2/react/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { CheckoutData } from './useCheckout';
|
|
3
|
+
export interface Address {
|
|
4
|
+
address1: string;
|
|
5
|
+
address2?: string;
|
|
6
|
+
lastName?: string;
|
|
7
|
+
firstName?: string;
|
|
8
|
+
city?: string;
|
|
9
|
+
state?: string;
|
|
10
|
+
country?: string;
|
|
11
|
+
postal?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
}
|
|
15
|
+
type PaymentMethod = {
|
|
16
|
+
id: string;
|
|
17
|
+
type: string;
|
|
18
|
+
title: string;
|
|
19
|
+
iconUrl: string;
|
|
20
|
+
default: boolean;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
type ExpressOrderLineItem = {
|
|
24
|
+
label: string;
|
|
25
|
+
amount: string;
|
|
26
|
+
};
|
|
27
|
+
type ExpressShippingMethod = {
|
|
28
|
+
label: string;
|
|
29
|
+
amount: string;
|
|
30
|
+
identifier: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
};
|
|
33
|
+
export interface ExpressPaymentContextType {
|
|
34
|
+
applePayPaymentMethod?: PaymentMethod;
|
|
35
|
+
googlePayPaymentMethod?: PaymentMethod;
|
|
36
|
+
reComputeOrderSummary: () => Promise<{
|
|
37
|
+
lineItems: ExpressOrderLineItem[];
|
|
38
|
+
total: {
|
|
39
|
+
label: string;
|
|
40
|
+
amount: string;
|
|
41
|
+
};
|
|
42
|
+
shippingMethods: ExpressShippingMethod[];
|
|
43
|
+
} | undefined>;
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
availableExpressPaymentMethodIds: string[];
|
|
46
|
+
setAvailableExpressPaymentMethodIds: (value: string[]) => void;
|
|
47
|
+
shippingMethods: ExpressShippingMethod[];
|
|
48
|
+
lineItems: ExpressOrderLineItem[];
|
|
49
|
+
handleAddExpressId: (id: string) => void;
|
|
50
|
+
updateCheckoutSessionValues: (input: {
|
|
51
|
+
data: {
|
|
52
|
+
shippingAddress: Address;
|
|
53
|
+
billingAddress?: Address | null;
|
|
54
|
+
};
|
|
55
|
+
}) => Promise<void>;
|
|
56
|
+
updateCustomerEmail: (input: {
|
|
57
|
+
data: {
|
|
58
|
+
email: string;
|
|
59
|
+
};
|
|
60
|
+
}) => Promise<void>;
|
|
61
|
+
error: string | null;
|
|
62
|
+
setError: (error: string | null) => void;
|
|
63
|
+
}
|
|
64
|
+
interface ExpressPaymentProviderProps {
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
customerId?: string;
|
|
67
|
+
checkout?: CheckoutData;
|
|
68
|
+
}
|
|
69
|
+
export declare const ExpressPaymentProvider: React.FC<ExpressPaymentProviderProps>;
|
|
70
|
+
export declare const useExpressPayment: () => ExpressPaymentContextType;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
|
3
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
4
|
+
import { useOrderSummary } from './useOrderSummary';
|
|
5
|
+
import { useShippingRates } from './useShippingRates';
|
|
6
|
+
const ExpressPaymentContext = createContext(undefined);
|
|
7
|
+
export const ExpressPaymentProvider = ({ children, customerId, checkout, }) => {
|
|
8
|
+
const { apiService } = useTagadaContext();
|
|
9
|
+
const [availableExpressPaymentMethodIds, setAvailableExpressPaymentMethodIds] = useState([]);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [paymentMethods, setPaymentMethods] = useState(null);
|
|
12
|
+
const [isLoadingPaymentMethods, setIsLoadingPaymentMethods] = useState(false);
|
|
13
|
+
const checkoutSessionId = checkout?.checkoutSession.id;
|
|
14
|
+
// Fetch enabled payment methods for this checkout session
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
let mounted = true;
|
|
17
|
+
const fetchPaymentMethods = async () => {
|
|
18
|
+
try {
|
|
19
|
+
if (!checkoutSessionId)
|
|
20
|
+
return;
|
|
21
|
+
setIsLoadingPaymentMethods(true);
|
|
22
|
+
const response = await apiService.fetch(`/api/v1/payment-methods?checkoutSessionId=${encodeURIComponent(checkoutSessionId)}`);
|
|
23
|
+
if (mounted)
|
|
24
|
+
setPaymentMethods(response);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (mounted)
|
|
28
|
+
setPaymentMethods([]);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
if (mounted)
|
|
32
|
+
setIsLoadingPaymentMethods(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (checkout)
|
|
36
|
+
void fetchPaymentMethods();
|
|
37
|
+
return () => {
|
|
38
|
+
mounted = false;
|
|
39
|
+
};
|
|
40
|
+
}, [apiService, checkoutSessionId]);
|
|
41
|
+
const handleAddExpressId = (id) => {
|
|
42
|
+
setAvailableExpressPaymentMethodIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
|
|
43
|
+
};
|
|
44
|
+
// Base data hooks
|
|
45
|
+
const { orderSummary, isLoading: isLoadingOrderSummary, isRefetching: isRefetchingOrderSummary, refetch: refetchOrderSummary, } = useOrderSummary({ sessionId: checkoutSessionId });
|
|
46
|
+
const { shippingRates, refetch: refetchRates } = useShippingRates({ checkout });
|
|
47
|
+
const minorUnitsToCurrencyString = (amountMinor, currency) => {
|
|
48
|
+
if (!amountMinor || !currency)
|
|
49
|
+
return '0.00';
|
|
50
|
+
return (amountMinor / 100).toFixed(2);
|
|
51
|
+
};
|
|
52
|
+
const shippingMethods = useMemo(() => (shippingRates || []).map((rate) => ({
|
|
53
|
+
label: rate.shippingRateName,
|
|
54
|
+
amount: minorUnitsToCurrencyString(rate.amount, rate.currency),
|
|
55
|
+
identifier: rate.id,
|
|
56
|
+
detail: rate.description || '',
|
|
57
|
+
})), [shippingRates]);
|
|
58
|
+
const lineItems = useMemo(() => [
|
|
59
|
+
{
|
|
60
|
+
label: 'Subtotal',
|
|
61
|
+
amount: minorUnitsToCurrencyString(orderSummary?.subtotalAdjustedAmount, orderSummary?.currency),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: 'Shipping',
|
|
65
|
+
amount: minorUnitsToCurrencyString(orderSummary?.shippingCost ?? 0, orderSummary?.currency),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: 'Tax',
|
|
69
|
+
amount: minorUnitsToCurrencyString(orderSummary?.totalTaxAmount, orderSummary?.currency),
|
|
70
|
+
},
|
|
71
|
+
], [
|
|
72
|
+
orderSummary?.subtotalAdjustedAmount,
|
|
73
|
+
orderSummary?.shippingCost,
|
|
74
|
+
orderSummary?.totalTaxAmount,
|
|
75
|
+
orderSummary?.currency,
|
|
76
|
+
]);
|
|
77
|
+
const reComputeOrderSummary = useCallback(async () => {
|
|
78
|
+
try {
|
|
79
|
+
await refetchOrderSummary();
|
|
80
|
+
await refetchRates();
|
|
81
|
+
if (!orderSummary || !shippingRates)
|
|
82
|
+
return;
|
|
83
|
+
const recomputedLineItems = [
|
|
84
|
+
{
|
|
85
|
+
label: 'Subtotal',
|
|
86
|
+
amount: minorUnitsToCurrencyString(orderSummary.subtotalAdjustedAmount, orderSummary.currency),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Shipping',
|
|
90
|
+
amount: minorUnitsToCurrencyString(orderSummary.shippingCost ?? 0, orderSummary.currency),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: 'Tax',
|
|
94
|
+
amount: minorUnitsToCurrencyString(orderSummary.totalTaxAmount, orderSummary.currency),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
const total = {
|
|
98
|
+
label: 'Order Total',
|
|
99
|
+
amount: minorUnitsToCurrencyString(orderSummary.totalAdjustedAmount, orderSummary.currency),
|
|
100
|
+
};
|
|
101
|
+
const recomputedShippingMethods = (shippingRates || []).map((rate) => ({
|
|
102
|
+
label: rate.shippingRateName,
|
|
103
|
+
amount: minorUnitsToCurrencyString(rate.amount, rate.currency),
|
|
104
|
+
identifier: rate.id,
|
|
105
|
+
detail: rate.description || '',
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
lineItems: recomputedLineItems,
|
|
109
|
+
total,
|
|
110
|
+
shippingMethods: recomputedShippingMethods,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}, [orderSummary, shippingRates, refetchOrderSummary, refetchRates]);
|
|
117
|
+
const updateCheckoutSessionValues = useCallback(async (input) => {
|
|
118
|
+
await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/address`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: input,
|
|
121
|
+
});
|
|
122
|
+
}, [apiService, checkoutSessionId]);
|
|
123
|
+
const updateCustomerEmail = useCallback(async (input) => {
|
|
124
|
+
if (!customerId)
|
|
125
|
+
return;
|
|
126
|
+
await apiService.fetch(`/api/v1/customers/${customerId}`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: input,
|
|
129
|
+
});
|
|
130
|
+
}, [apiService, customerId]);
|
|
131
|
+
const enabledApplePayPaymentMethod = useMemo(() => paymentMethods?.find((p) => p.type === 'apple_pay'), [paymentMethods]);
|
|
132
|
+
const enabledGooglePayPaymentMethod = useMemo(() => paymentMethods?.find((p) => p.type === 'google_pay'), [paymentMethods]);
|
|
133
|
+
const loading = !paymentMethods || isLoadingPaymentMethods || isLoadingOrderSummary;
|
|
134
|
+
const contextValue = {
|
|
135
|
+
availableExpressPaymentMethodIds,
|
|
136
|
+
setAvailableExpressPaymentMethodIds,
|
|
137
|
+
applePayPaymentMethod: enabledApplePayPaymentMethod,
|
|
138
|
+
googlePayPaymentMethod: enabledGooglePayPaymentMethod,
|
|
139
|
+
shippingMethods,
|
|
140
|
+
lineItems,
|
|
141
|
+
reComputeOrderSummary,
|
|
142
|
+
loading,
|
|
143
|
+
handleAddExpressId,
|
|
144
|
+
updateCheckoutSessionValues,
|
|
145
|
+
updateCustomerEmail,
|
|
146
|
+
error,
|
|
147
|
+
setError,
|
|
148
|
+
};
|
|
149
|
+
const hasAnyEnabled = Boolean(enabledApplePayPaymentMethod || enabledGooglePayPaymentMethod);
|
|
150
|
+
return (_jsx(ExpressPaymentContext.Provider, { value: contextValue, children: hasAnyEnabled ? _jsx(_Fragment, { children: children }) : _jsx(_Fragment, {}) }));
|
|
151
|
+
};
|
|
152
|
+
export const useExpressPayment = () => {
|
|
153
|
+
const context = useContext(ExpressPaymentContext);
|
|
154
|
+
if (context === undefined) {
|
|
155
|
+
throw new Error('useExpressPayment must be used within an ExpressPaymentProvider');
|
|
156
|
+
}
|
|
157
|
+
return context;
|
|
158
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funnel Resource - API client for funnel navigation and session management
|
|
3
|
+
*/
|
|
4
|
+
import { ApiClient } from './apiClient';
|
|
5
|
+
export interface FunnelEvent {
|
|
6
|
+
type: string;
|
|
7
|
+
data?: any;
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FunnelNavigationAction {
|
|
11
|
+
type: 'redirect' | 'replace' | 'push' | 'external' | 'none';
|
|
12
|
+
url?: string;
|
|
13
|
+
data?: any;
|
|
14
|
+
}
|
|
15
|
+
export interface FunnelNavigationResult {
|
|
16
|
+
stepId: string;
|
|
17
|
+
action: FunnelNavigationAction;
|
|
18
|
+
context: SimpleFunnelContext;
|
|
19
|
+
tracking?: {
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
event: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface SimpleFunnelContext {
|
|
27
|
+
customerId: string;
|
|
28
|
+
storeId: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
funnelId: string;
|
|
31
|
+
currentStepId: string;
|
|
32
|
+
previousStepId?: string;
|
|
33
|
+
startedAt: number;
|
|
34
|
+
lastActivityAt: number;
|
|
35
|
+
metadata?: Record<string, any>;
|
|
36
|
+
}
|
|
37
|
+
export interface FunnelInitializeRequest {
|
|
38
|
+
cmsSession: {
|
|
39
|
+
customerId: string;
|
|
40
|
+
storeId: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
accountId: string;
|
|
43
|
+
};
|
|
44
|
+
funnelId?: string;
|
|
45
|
+
entryStepId?: string;
|
|
46
|
+
existingSessionId?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface FunnelInitializeResponse {
|
|
49
|
+
success: boolean;
|
|
50
|
+
context?: SimpleFunnelContext;
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface FunnelNavigateRequest {
|
|
54
|
+
sessionId: string;
|
|
55
|
+
event: FunnelEvent;
|
|
56
|
+
contextUpdates?: Partial<SimpleFunnelContext>;
|
|
57
|
+
}
|
|
58
|
+
export interface FunnelNavigateResponse {
|
|
59
|
+
success: boolean;
|
|
60
|
+
result?: {
|
|
61
|
+
stepId: string;
|
|
62
|
+
url?: string;
|
|
63
|
+
tracking?: {
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
event: string;
|
|
67
|
+
timestamp: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface FunnelContextUpdateRequest {
|
|
73
|
+
contextUpdates: Partial<SimpleFunnelContext>;
|
|
74
|
+
}
|
|
75
|
+
export interface FunnelContextUpdateResponse {
|
|
76
|
+
success: boolean;
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
export declare class FunnelResource {
|
|
80
|
+
private apiClient;
|
|
81
|
+
constructor(apiClient: ApiClient);
|
|
82
|
+
/**
|
|
83
|
+
* Initialize a funnel session
|
|
84
|
+
*/
|
|
85
|
+
initialize(request: FunnelInitializeRequest): Promise<FunnelInitializeResponse>;
|
|
86
|
+
/**
|
|
87
|
+
* Navigate to next step in funnel
|
|
88
|
+
*/
|
|
89
|
+
navigate(request: FunnelNavigateRequest): Promise<FunnelNavigateResponse>;
|
|
90
|
+
/**
|
|
91
|
+
* Update funnel context
|
|
92
|
+
*/
|
|
93
|
+
updateContext(sessionId: string, request: FunnelContextUpdateRequest): Promise<FunnelContextUpdateResponse>;
|
|
94
|
+
/**
|
|
95
|
+
* End funnel session
|
|
96
|
+
*/
|
|
97
|
+
endSession(sessionId: string): Promise<{
|
|
98
|
+
success: boolean;
|
|
99
|
+
error?: string;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Get funnel session by ID
|
|
103
|
+
*/
|
|
104
|
+
getSession(sessionId: string): Promise<{
|
|
105
|
+
success: boolean;
|
|
106
|
+
context?: SimpleFunnelContext;
|
|
107
|
+
error?: string;
|
|
108
|
+
}>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funnel Resource - API client for funnel navigation and session management
|
|
3
|
+
*/
|
|
4
|
+
export class FunnelResource {
|
|
5
|
+
constructor(apiClient) {
|
|
6
|
+
this.apiClient = apiClient;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Initialize a funnel session
|
|
10
|
+
*/
|
|
11
|
+
async initialize(request) {
|
|
12
|
+
return this.apiClient.post('/api/v1/funnel/initialize', request);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Navigate to next step in funnel
|
|
16
|
+
*/
|
|
17
|
+
async navigate(request) {
|
|
18
|
+
return this.apiClient.post('/api/v1/funnel/navigate', request);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Update funnel context
|
|
22
|
+
*/
|
|
23
|
+
async updateContext(sessionId, request) {
|
|
24
|
+
return this.apiClient.patch(`/api/v1/funnel/context/${sessionId}`, request);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* End funnel session
|
|
28
|
+
*/
|
|
29
|
+
async endSession(sessionId) {
|
|
30
|
+
return this.apiClient.delete(`/api/v1/funnel/session/${sessionId}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get funnel session by ID
|
|
34
|
+
*/
|
|
35
|
+
async getSession(sessionId) {
|
|
36
|
+
return this.apiClient.get(`/api/v1/funnel/session/${sessionId}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/v2/index.d.ts
CHANGED
|
@@ -20,4 +20,6 @@ export type { ShippingRate, ShippingRatesResponse } from './core/resources/shipp
|
|
|
20
20
|
export type { ApplyDiscountResponse, Discount, DiscountCodeValidation, RemoveDiscountResponse } from './core/resources/discounts';
|
|
21
21
|
export type { ToggleOrderBumpResponse, VipOffer, VipPreviewResponse } from './core/resources/vipOffers';
|
|
22
22
|
export type { StoreConfig } from './core/resources/storeConfig';
|
|
23
|
-
export {
|
|
23
|
+
export type { FunnelContextUpdateRequest, FunnelContextUpdateResponse, FunnelEvent, FunnelInitializeRequest, FunnelInitializeResponse, FunnelNavigateRequest, FunnelNavigateResponse, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext } from './core/resources/funnel';
|
|
24
|
+
export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useCheckout, useCheckoutToken, useCountryOptions, useCurrency, useDiscountQuery, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useVipOffers } from './react';
|
|
25
|
+
export type { StoreDiscount } from './react';
|
package/dist/v2/index.js
CHANGED
|
@@ -12,4 +12,4 @@ export * from './core/utils/currency';
|
|
|
12
12
|
export * from './core/utils/pluginConfig';
|
|
13
13
|
export * from './core/utils/products';
|
|
14
14
|
// React exports (hooks and components only, types are exported above)
|
|
15
|
-
export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useCheckout, useCheckoutToken, useCountryOptions, useCurrency, useDiscounts, useExpressPaymentMethods, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useShippingRates, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useVipOffers } from './react';
|
|
15
|
+
export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useCheckout, useCheckoutToken, useCountryOptions, useCurrency, useDiscountQuery, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useVipOffers } from './react';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single Discount Hook using TanStack Query
|
|
3
|
+
* Fetches a specific discount for a given store
|
|
4
|
+
*/
|
|
5
|
+
import { UseQueryResult } from '@tanstack/react-query';
|
|
6
|
+
export interface StoreDiscountRuleAmount {
|
|
7
|
+
rate: number;
|
|
8
|
+
amount: number;
|
|
9
|
+
lock: boolean;
|
|
10
|
+
date: string;
|
|
11
|
+
}
|
|
12
|
+
export interface StoreDiscountRule {
|
|
13
|
+
id: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
promotionId: string;
|
|
17
|
+
type: string;
|
|
18
|
+
productId: string | null;
|
|
19
|
+
minimumQuantity: number | null;
|
|
20
|
+
minimumAmount: Record<string, StoreDiscountRuleAmount> | null;
|
|
21
|
+
variantIds: string[] | null;
|
|
22
|
+
}
|
|
23
|
+
export interface StoreDiscountAction {
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
promotionId: string;
|
|
28
|
+
type: string;
|
|
29
|
+
adjustmentAmount: number | null;
|
|
30
|
+
adjustmentPercentage: number | null;
|
|
31
|
+
adjustmentType: string | null;
|
|
32
|
+
freeShipping: boolean | null;
|
|
33
|
+
priceIdToAdd: string | null;
|
|
34
|
+
productIdToAdd: string | null;
|
|
35
|
+
variantIdToAdd: string | null;
|
|
36
|
+
subscriptionFreeTrialDuration: number | null;
|
|
37
|
+
subscriptionFreeTrialDurationType: string | null;
|
|
38
|
+
targetProductId: string | null;
|
|
39
|
+
targetVariantIds: string[] | null;
|
|
40
|
+
maxQuantityDiscounted: number | null;
|
|
41
|
+
appliesOnEachItem: boolean | null;
|
|
42
|
+
}
|
|
43
|
+
export interface StoreDiscount {
|
|
44
|
+
id: string;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
updatedAt: string;
|
|
47
|
+
storeId: string;
|
|
48
|
+
accountId: string;
|
|
49
|
+
name: string;
|
|
50
|
+
code: string;
|
|
51
|
+
automatic: boolean;
|
|
52
|
+
usageLimit: number | null;
|
|
53
|
+
usageCount: number;
|
|
54
|
+
startDate: string;
|
|
55
|
+
endDate: string | null;
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
archived: boolean;
|
|
58
|
+
ruleOperator: string;
|
|
59
|
+
externalId: string | null;
|
|
60
|
+
combinesWithOrderLevelDiscounts: boolean;
|
|
61
|
+
combinesWithLineItemDiscounts: boolean;
|
|
62
|
+
combinesWithShippingDiscounts: boolean;
|
|
63
|
+
forceCombine: boolean;
|
|
64
|
+
isTemporary: boolean;
|
|
65
|
+
rules: StoreDiscountRule[];
|
|
66
|
+
actions: StoreDiscountAction[];
|
|
67
|
+
}
|
|
68
|
+
export interface UseDiscountQueryOptions {
|
|
69
|
+
storeId?: string;
|
|
70
|
+
discountId?: string;
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface UseDiscountQueryResult<TData = StoreDiscount> {
|
|
74
|
+
discount: TData | undefined;
|
|
75
|
+
isLoading: boolean;
|
|
76
|
+
error: Error | null;
|
|
77
|
+
refetch: UseQueryResult<TData>['refetch'];
|
|
78
|
+
}
|
|
79
|
+
export declare function useDiscountQuery<TData = StoreDiscount>(options: UseDiscountQueryOptions): UseDiscountQueryResult<TData>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single Discount Hook using TanStack Query
|
|
3
|
+
* Fetches a specific discount for a given store
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
import { useApiQuery } from './useApiQuery';
|
|
7
|
+
import { usePluginConfig } from './usePluginConfig';
|
|
8
|
+
export function useDiscountQuery(options) {
|
|
9
|
+
const { storeId: storeIdFromConfig } = usePluginConfig();
|
|
10
|
+
const { storeId = storeIdFromConfig, discountId, enabled = true } = options;
|
|
11
|
+
const key = useMemo(() => ['discount', storeId, discountId], [storeId, discountId]);
|
|
12
|
+
const url = useMemo(() => {
|
|
13
|
+
if (!storeId || !discountId)
|
|
14
|
+
return null;
|
|
15
|
+
return `/api/v1/stores/${storeId}/discounts/${discountId}`;
|
|
16
|
+
}, [storeId, discountId]);
|
|
17
|
+
const query = useApiQuery(key, url, { enabled });
|
|
18
|
+
return {
|
|
19
|
+
discount: query.data,
|
|
20
|
+
isLoading: query.isLoading,
|
|
21
|
+
error: query.error || null,
|
|
22
|
+
refetch: query.refetch,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFunnel Hook (v2) - TanStack Query-based funnel navigation
|
|
3
|
+
*
|
|
4
|
+
* Modern implementation using TanStack Query for state management
|
|
5
|
+
* and the v2 ApiClient for API calls.
|
|
6
|
+
*/
|
|
7
|
+
import { FunnelEvent, FunnelNavigationResult, SimpleFunnelContext } from '../../core/resources/funnel';
|
|
8
|
+
export interface UseFunnelOptions {
|
|
9
|
+
funnelId?: string;
|
|
10
|
+
currentStepId?: string;
|
|
11
|
+
onNavigate?: (result: FunnelNavigationResult) => void | boolean;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
autoInitialize?: boolean;
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface UseFunnelResult {
|
|
17
|
+
next: (event: FunnelEvent) => Promise<any>;
|
|
18
|
+
goToStep: (stepId: string) => Promise<any>;
|
|
19
|
+
updateContext: (updates: Partial<SimpleFunnelContext>) => Promise<void>;
|
|
20
|
+
currentStep: {
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
context: SimpleFunnelContext | null;
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
isInitialized: boolean;
|
|
26
|
+
initializeSession: (entryStepId?: string) => Promise<void>;
|
|
27
|
+
endSession: () => Promise<void>;
|
|
28
|
+
retryInitialization: () => Promise<void>;
|
|
29
|
+
initializationError: Error | null;
|
|
30
|
+
isSessionLoading: boolean;
|
|
31
|
+
sessionError: Error | null;
|
|
32
|
+
refetch: () => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* React Hook for Funnel Navigation (Plugin SDK v2)
|
|
36
|
+
*
|
|
37
|
+
* Modern funnel navigation using TanStack Query for state management
|
|
38
|
+
* and the v2 ApiClient architecture.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useFunnel(options: UseFunnelOptions): UseFunnelResult;
|
|
41
|
+
/**
|
|
42
|
+
* Simplified funnel hook for basic step tracking (v2)
|
|
43
|
+
*/
|
|
44
|
+
export declare function useSimpleFunnel(funnelId: string, initialStepId?: string): {
|
|
45
|
+
currentStepId: string;
|
|
46
|
+
next: (event: FunnelEvent) => Promise<any>;
|
|
47
|
+
goToStep: (stepId: string) => Promise<any>;
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
context: SimpleFunnelContext | null;
|
|
50
|
+
};
|
|
51
|
+
export type { FunnelEvent, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext } from '../../core/resources/funnel';
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFunnel Hook (v2) - TanStack Query-based funnel navigation
|
|
3
|
+
*
|
|
4
|
+
* Modern implementation using TanStack Query for state management
|
|
5
|
+
* and the v2 ApiClient for API calls.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
8
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
10
|
+
import { FunnelResource } from '../../core/resources/funnel';
|
|
11
|
+
import { getGlobalApiClient } from './useApiQuery';
|
|
12
|
+
// Query keys for funnel operations
|
|
13
|
+
const funnelQueryKeys = {
|
|
14
|
+
session: (sessionId) => ['funnel', 'session', sessionId],
|
|
15
|
+
context: (sessionId) => ['funnel', 'context', sessionId],
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* React Hook for Funnel Navigation (Plugin SDK v2)
|
|
19
|
+
*
|
|
20
|
+
* Modern funnel navigation using TanStack Query for state management
|
|
21
|
+
* and the v2 ApiClient architecture.
|
|
22
|
+
*/
|
|
23
|
+
export function useFunnel(options) {
|
|
24
|
+
const { auth, store } = useTagadaContext();
|
|
25
|
+
const queryClient = useQueryClient();
|
|
26
|
+
const apiClient = getGlobalApiClient();
|
|
27
|
+
const funnelResource = useMemo(() => new FunnelResource(apiClient), [apiClient]);
|
|
28
|
+
// Local state
|
|
29
|
+
const [context, setContext] = useState(null);
|
|
30
|
+
const [initializationAttempted, setInitializationAttempted] = useState(false);
|
|
31
|
+
const [initializationError, setInitializationError] = useState(null);
|
|
32
|
+
const currentStepId = options.currentStepId || context?.currentStepId;
|
|
33
|
+
// Check for URL parameter overrides
|
|
34
|
+
const urlParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams();
|
|
35
|
+
const urlFunnelId = urlParams.get('funnelId') || undefined;
|
|
36
|
+
const effectiveFunnelId = urlFunnelId || options.funnelId;
|
|
37
|
+
// Session query - only enabled when we have a session ID
|
|
38
|
+
const { data: sessionData, isLoading: isSessionLoading, error: sessionError, refetch: refetchSession } = useQuery({
|
|
39
|
+
queryKey: funnelQueryKeys.session(context?.sessionId || ''),
|
|
40
|
+
queryFn: async () => {
|
|
41
|
+
if (!context?.sessionId) {
|
|
42
|
+
console.warn('🍪 Funnel: No session ID available for query');
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
console.log(`🍪 Funnel: Fetching session data for ID: ${context.sessionId}`);
|
|
46
|
+
const response = await funnelResource.getSession(context.sessionId);
|
|
47
|
+
console.log(`🍪 Funnel: Session fetch response:`, response);
|
|
48
|
+
if (response.success && response.context) {
|
|
49
|
+
return response.context;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(response.error || 'Failed to fetch session');
|
|
52
|
+
},
|
|
53
|
+
enabled: !!context?.sessionId && (options.enabled !== false),
|
|
54
|
+
staleTime: 30000, // 30 seconds
|
|
55
|
+
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
56
|
+
refetchOnWindowFocus: false,
|
|
57
|
+
});
|
|
58
|
+
// Initialize session mutation
|
|
59
|
+
const initializeMutation = useMutation({
|
|
60
|
+
mutationFn: async (entryStepId) => {
|
|
61
|
+
if (!auth.session?.customerId || !store?.id) {
|
|
62
|
+
throw new Error('Authentication required for funnel session');
|
|
63
|
+
}
|
|
64
|
+
// Check for existing session ID in URL parameters
|
|
65
|
+
let existingSessionId = urlParams.get('funnelSessionId') || undefined;
|
|
66
|
+
if (existingSessionId) {
|
|
67
|
+
console.log(`🍪 Funnel: Found session ID in URL params: ${existingSessionId}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Fallback to cookie for same-domain scenarios
|
|
71
|
+
const funnelSessionCookie = document.cookie
|
|
72
|
+
.split('; ')
|
|
73
|
+
.find(row => row.startsWith('tgd-funnel-session-id='));
|
|
74
|
+
existingSessionId = funnelSessionCookie ? funnelSessionCookie.split('=')[1] : undefined;
|
|
75
|
+
if (existingSessionId) {
|
|
76
|
+
console.log(`🍪 Funnel: Found session in cookie: ${existingSessionId}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!existingSessionId) {
|
|
80
|
+
console.log(`🍪 Funnel: No existing session found in URL params or cookie`);
|
|
81
|
+
}
|
|
82
|
+
// Send minimal CMS session data
|
|
83
|
+
const cmsSessionData = {
|
|
84
|
+
customerId: auth.session.customerId,
|
|
85
|
+
storeId: store.id,
|
|
86
|
+
sessionId: auth.session.sessionId,
|
|
87
|
+
accountId: auth.session.accountId
|
|
88
|
+
};
|
|
89
|
+
// Call API to initialize session - backend will restore existing or create new
|
|
90
|
+
const requestBody = {
|
|
91
|
+
cmsSession: cmsSessionData,
|
|
92
|
+
entryStepId, // Optional override
|
|
93
|
+
existingSessionId // Pass existing session ID from URL or cookie
|
|
94
|
+
};
|
|
95
|
+
// Only include funnelId if it's provided (for backend fallback)
|
|
96
|
+
if (effectiveFunnelId) {
|
|
97
|
+
requestBody.funnelId = effectiveFunnelId;
|
|
98
|
+
}
|
|
99
|
+
const response = await funnelResource.initialize(requestBody);
|
|
100
|
+
if (response.success && response.context) {
|
|
101
|
+
return response.context;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error(response.error || 'Failed to initialize funnel session');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onSuccess: (newContext) => {
|
|
108
|
+
setContext(newContext);
|
|
109
|
+
setInitializationError(null);
|
|
110
|
+
// Set session cookie for persistence across page reloads
|
|
111
|
+
setSessionCookie(newContext.sessionId);
|
|
112
|
+
console.log(`🍪 Funnel: Initialized session for funnel ${effectiveFunnelId || 'default'}`, newContext);
|
|
113
|
+
// Invalidate session query to refetch with new session ID
|
|
114
|
+
void queryClient.invalidateQueries({
|
|
115
|
+
queryKey: funnelQueryKeys.session(newContext.sessionId)
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
onError: (error) => {
|
|
119
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
120
|
+
setInitializationError(errorObj);
|
|
121
|
+
console.error('Error initializing funnel session:', error);
|
|
122
|
+
if (options.onError) {
|
|
123
|
+
options.onError(errorObj);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Navigate mutation
|
|
128
|
+
const navigateMutation = useMutation({
|
|
129
|
+
mutationFn: async (event) => {
|
|
130
|
+
if (!context) {
|
|
131
|
+
throw new Error('Funnel session not initialized');
|
|
132
|
+
}
|
|
133
|
+
if (!context.sessionId) {
|
|
134
|
+
throw new Error('Funnel session ID missing - session may be corrupted');
|
|
135
|
+
}
|
|
136
|
+
console.log(`🍪 Funnel: Navigating with session ID: ${context.sessionId}`);
|
|
137
|
+
const requestBody = {
|
|
138
|
+
sessionId: context.sessionId,
|
|
139
|
+
event: {
|
|
140
|
+
type: event.type,
|
|
141
|
+
data: event.data,
|
|
142
|
+
timestamp: event.timestamp || new Date().toISOString()
|
|
143
|
+
},
|
|
144
|
+
contextUpdates: {
|
|
145
|
+
lastActivityAt: Date.now()
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const response = await funnelResource.navigate(requestBody);
|
|
149
|
+
if (response.success && response.result) {
|
|
150
|
+
return response.result;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
throw new Error(response.error || 'Navigation failed');
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
onSuccess: (result) => {
|
|
157
|
+
if (!context)
|
|
158
|
+
return;
|
|
159
|
+
// Update local context
|
|
160
|
+
const newContext = {
|
|
161
|
+
...context,
|
|
162
|
+
currentStepId: result.stepId,
|
|
163
|
+
previousStepId: context.currentStepId,
|
|
164
|
+
lastActivityAt: Date.now(),
|
|
165
|
+
metadata: {
|
|
166
|
+
...context.metadata,
|
|
167
|
+
lastEvent: 'navigation',
|
|
168
|
+
lastTransition: new Date().toISOString()
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
setContext(newContext);
|
|
172
|
+
// Create typed navigation result
|
|
173
|
+
const navigationResult = {
|
|
174
|
+
stepId: result.stepId,
|
|
175
|
+
action: {
|
|
176
|
+
type: 'redirect', // Default action type
|
|
177
|
+
url: result.url
|
|
178
|
+
},
|
|
179
|
+
context: newContext,
|
|
180
|
+
tracking: result.tracking
|
|
181
|
+
};
|
|
182
|
+
// Handle navigation callback with override capability
|
|
183
|
+
let shouldPerformDefaultNavigation = true;
|
|
184
|
+
if (options.onNavigate) {
|
|
185
|
+
const callbackResult = options.onNavigate(navigationResult);
|
|
186
|
+
if (callbackResult === false) {
|
|
187
|
+
shouldPerformDefaultNavigation = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Perform default navigation if not overridden
|
|
191
|
+
if (shouldPerformDefaultNavigation && navigationResult.action.url) {
|
|
192
|
+
// Add URL parameters for cross-domain session continuity
|
|
193
|
+
const urlWithParams = addSessionParams(navigationResult.action.url, newContext.sessionId, effectiveFunnelId || options.funnelId);
|
|
194
|
+
const updatedAction = { ...navigationResult.action, url: urlWithParams };
|
|
195
|
+
performNavigation(updatedAction);
|
|
196
|
+
}
|
|
197
|
+
console.log(`🍪 Funnel: Navigated from ${context.currentStepId} to ${result.stepId}`);
|
|
198
|
+
// Invalidate and refetch session data
|
|
199
|
+
void queryClient.invalidateQueries({
|
|
200
|
+
queryKey: funnelQueryKeys.session(newContext.sessionId)
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
onError: (error) => {
|
|
204
|
+
console.error('Funnel navigation error:', error);
|
|
205
|
+
if (options.onError) {
|
|
206
|
+
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Update context mutation
|
|
211
|
+
const updateContextMutation = useMutation({
|
|
212
|
+
mutationFn: async (updates) => {
|
|
213
|
+
if (!context) {
|
|
214
|
+
throw new Error('Funnel session not initialized');
|
|
215
|
+
}
|
|
216
|
+
const requestBody = {
|
|
217
|
+
contextUpdates: updates
|
|
218
|
+
};
|
|
219
|
+
const response = await funnelResource.updateContext(context.sessionId, requestBody);
|
|
220
|
+
if (response.success) {
|
|
221
|
+
return updates;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
throw new Error(response.error || 'Context update failed');
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
onSuccess: (updates) => {
|
|
228
|
+
if (!context)
|
|
229
|
+
return;
|
|
230
|
+
const updatedContext = {
|
|
231
|
+
...context,
|
|
232
|
+
...updates,
|
|
233
|
+
lastActivityAt: Date.now()
|
|
234
|
+
};
|
|
235
|
+
setContext(updatedContext);
|
|
236
|
+
console.log(`🍪 Funnel: Updated context for step ${context.currentStepId}`);
|
|
237
|
+
// Invalidate session query
|
|
238
|
+
void queryClient.invalidateQueries({
|
|
239
|
+
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
onError: (error) => {
|
|
243
|
+
console.error('Error updating funnel context:', error);
|
|
244
|
+
if (options.onError) {
|
|
245
|
+
options.onError(error instanceof Error ? error : new Error(String(error)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// End session mutation
|
|
250
|
+
const endSessionMutation = useMutation({
|
|
251
|
+
mutationFn: async () => {
|
|
252
|
+
if (!context)
|
|
253
|
+
return;
|
|
254
|
+
await funnelResource.endSession(context.sessionId);
|
|
255
|
+
},
|
|
256
|
+
onSuccess: () => {
|
|
257
|
+
if (!context)
|
|
258
|
+
return;
|
|
259
|
+
console.log(`🍪 Funnel: Ended session ${context.sessionId}`);
|
|
260
|
+
setContext(null);
|
|
261
|
+
// Clear queries
|
|
262
|
+
queryClient.removeQueries({
|
|
263
|
+
queryKey: funnelQueryKeys.session(context.sessionId)
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
onError: (error) => {
|
|
267
|
+
console.error('Error ending funnel session:', error);
|
|
268
|
+
// Don't throw here - session ending is best effort
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* Set funnel session cookie
|
|
273
|
+
*/
|
|
274
|
+
const setSessionCookie = useCallback((sessionId) => {
|
|
275
|
+
const maxAge = 24 * 60 * 60; // 24 hours
|
|
276
|
+
const expires = new Date(Date.now() + maxAge * 1000).toUTCString();
|
|
277
|
+
// Set cookie for same-domain scenarios
|
|
278
|
+
document.cookie = `tgd-funnel-session-id=${sessionId}; path=/; expires=${expires}; SameSite=Lax`;
|
|
279
|
+
console.log(`🍪 Funnel: Set session cookie: ${sessionId}`);
|
|
280
|
+
}, []);
|
|
281
|
+
/**
|
|
282
|
+
* Add session parameters to URL for cross-domain continuity
|
|
283
|
+
*/
|
|
284
|
+
const addSessionParams = useCallback((url, sessionId, funnelId) => {
|
|
285
|
+
try {
|
|
286
|
+
const urlObj = new URL(url);
|
|
287
|
+
urlObj.searchParams.set('funnelSessionId', sessionId);
|
|
288
|
+
if (funnelId) {
|
|
289
|
+
urlObj.searchParams.set('funnelId', funnelId);
|
|
290
|
+
}
|
|
291
|
+
const urlWithParams = urlObj.toString();
|
|
292
|
+
console.log(`🍪 Funnel: Added session params to URL: ${url} → ${urlWithParams}`);
|
|
293
|
+
return urlWithParams;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
console.warn('Failed to add session params to URL:', error);
|
|
297
|
+
return url; // Return original URL if parsing fails
|
|
298
|
+
}
|
|
299
|
+
}, []);
|
|
300
|
+
/**
|
|
301
|
+
* Perform navigation based on action type
|
|
302
|
+
*/
|
|
303
|
+
const performNavigation = useCallback((action) => {
|
|
304
|
+
if (!action.url)
|
|
305
|
+
return;
|
|
306
|
+
// Handle relative URLs by making them absolute
|
|
307
|
+
let targetUrl = action.url;
|
|
308
|
+
if (targetUrl.startsWith('/') && !targetUrl.startsWith('//')) {
|
|
309
|
+
// Relative URL - use current origin
|
|
310
|
+
targetUrl = window.location.origin + targetUrl;
|
|
311
|
+
}
|
|
312
|
+
switch (action.type) {
|
|
313
|
+
case 'redirect':
|
|
314
|
+
console.log(`🍪 Funnel: Redirecting to ${targetUrl}`);
|
|
315
|
+
window.location.href = targetUrl;
|
|
316
|
+
break;
|
|
317
|
+
case 'replace':
|
|
318
|
+
console.log(`🍪 Funnel: Replacing current page with ${targetUrl}`);
|
|
319
|
+
window.location.replace(targetUrl);
|
|
320
|
+
break;
|
|
321
|
+
case 'push':
|
|
322
|
+
console.log(`🍪 Funnel: Pushing to history: ${targetUrl}`);
|
|
323
|
+
window.history.pushState({}, '', targetUrl);
|
|
324
|
+
// Trigger a popstate event to update React Router
|
|
325
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
326
|
+
break;
|
|
327
|
+
case 'external':
|
|
328
|
+
console.log(`🍪 Funnel: Opening external URL: ${targetUrl}`);
|
|
329
|
+
window.open(targetUrl, '_blank');
|
|
330
|
+
break;
|
|
331
|
+
case 'none':
|
|
332
|
+
console.log(`🍪 Funnel: No navigation action required`);
|
|
333
|
+
break;
|
|
334
|
+
default:
|
|
335
|
+
console.warn(`🍪 Funnel: Unknown navigation action type: ${action.type}`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}, []);
|
|
339
|
+
// Public API methods
|
|
340
|
+
const initializeSession = useCallback(async (entryStepId) => {
|
|
341
|
+
setInitializationAttempted(true);
|
|
342
|
+
await initializeMutation.mutateAsync(entryStepId);
|
|
343
|
+
}, [initializeMutation]);
|
|
344
|
+
const next = useCallback(async (event) => {
|
|
345
|
+
return navigateMutation.mutateAsync(event);
|
|
346
|
+
}, [navigateMutation]);
|
|
347
|
+
const goToStep = useCallback(async (stepId) => {
|
|
348
|
+
return next({
|
|
349
|
+
type: 'direct_navigation',
|
|
350
|
+
data: { targetStepId: stepId },
|
|
351
|
+
timestamp: new Date().toISOString()
|
|
352
|
+
});
|
|
353
|
+
}, [next]);
|
|
354
|
+
const updateContext = useCallback(async (updates) => {
|
|
355
|
+
await updateContextMutation.mutateAsync(updates);
|
|
356
|
+
}, [updateContextMutation]);
|
|
357
|
+
const endSession = useCallback(async () => {
|
|
358
|
+
await endSessionMutation.mutateAsync();
|
|
359
|
+
}, [endSessionMutation]);
|
|
360
|
+
const retryInitialization = useCallback(async () => {
|
|
361
|
+
setInitializationAttempted(false);
|
|
362
|
+
setInitializationError(null);
|
|
363
|
+
await initializeSession();
|
|
364
|
+
}, [initializeSession]);
|
|
365
|
+
// Auto-initialize if requested and not already initialized
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (options.autoInitialize &&
|
|
368
|
+
!context &&
|
|
369
|
+
!initializationAttempted &&
|
|
370
|
+
!initializationError &&
|
|
371
|
+
auth.session?.customerId &&
|
|
372
|
+
store?.id &&
|
|
373
|
+
!initializeMutation.isPending) {
|
|
374
|
+
console.log('🍪 Funnel: Auto-initializing session...');
|
|
375
|
+
initializeSession().catch(error => {
|
|
376
|
+
console.error('Auto-initialization failed - will not retry:', error);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}, [
|
|
380
|
+
options.autoInitialize,
|
|
381
|
+
context,
|
|
382
|
+
initializationAttempted,
|
|
383
|
+
initializationError,
|
|
384
|
+
auth.session?.customerId,
|
|
385
|
+
store?.id,
|
|
386
|
+
initializeMutation.isPending,
|
|
387
|
+
initializeSession
|
|
388
|
+
]);
|
|
389
|
+
// Sync session data from query to local state
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (sessionData && sessionData !== context) {
|
|
392
|
+
setContext(sessionData);
|
|
393
|
+
}
|
|
394
|
+
}, [sessionData, context]);
|
|
395
|
+
const isLoading = initializeMutation.isPending || navigateMutation.isPending || updateContextMutation.isPending;
|
|
396
|
+
const isInitialized = !!context;
|
|
397
|
+
return {
|
|
398
|
+
next,
|
|
399
|
+
goToStep,
|
|
400
|
+
updateContext,
|
|
401
|
+
currentStep: {
|
|
402
|
+
id: currentStepId || 'unknown'
|
|
403
|
+
},
|
|
404
|
+
context,
|
|
405
|
+
isLoading,
|
|
406
|
+
isInitialized,
|
|
407
|
+
initializeSession,
|
|
408
|
+
endSession,
|
|
409
|
+
retryInitialization,
|
|
410
|
+
initializationError,
|
|
411
|
+
isSessionLoading,
|
|
412
|
+
sessionError,
|
|
413
|
+
refetch: () => void refetchSession()
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Simplified funnel hook for basic step tracking (v2)
|
|
418
|
+
*/
|
|
419
|
+
export function useSimpleFunnel(funnelId, initialStepId) {
|
|
420
|
+
const funnel = useFunnel({
|
|
421
|
+
funnelId,
|
|
422
|
+
currentStepId: initialStepId,
|
|
423
|
+
autoInitialize: true
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
currentStepId: funnel.currentStep.id,
|
|
427
|
+
next: funnel.next,
|
|
428
|
+
goToStep: funnel.goToStep,
|
|
429
|
+
isLoading: funnel.isLoading,
|
|
430
|
+
context: funnel.context
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Order Hook using TanStack Query
|
|
3
3
|
* Handles order creation and management with automatic caching
|
|
4
4
|
*/
|
|
5
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
5
6
|
import { useMemo } from 'react';
|
|
6
|
-
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
7
7
|
import { OrdersResource } from '../../core/resources/orders';
|
|
8
8
|
import { getGlobalApiClient } from './useApiQuery';
|
|
9
9
|
export function useOrderQuery(options = {}) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Post Purchases Hook using TanStack Query
|
|
3
3
|
* Handles post-purchase offers with automatic caching
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { CheckoutSessionState, CurrencyOptions, OrderSummary, PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferSummary } from '../../core/resources/postPurchases';
|
|
6
6
|
export interface UsePostPurchasesQueryOptions {
|
|
7
7
|
orderId: string;
|
|
8
8
|
enabled?: boolean;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Post Purchases Hook using TanStack Query
|
|
3
3
|
* Handles post-purchase offers with automatic caching
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
6
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { PostPurchasesResource } from '../../core/resources/postPurchases';
|
|
8
8
|
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
9
9
|
import { getGlobalApiClient } from './useApiQuery';
|
package/dist/v2/react/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { usePluginConfig } from './hooks/usePluginConfig';
|
|
|
15
15
|
export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
|
|
16
16
|
export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
|
|
17
17
|
export { useCurrency } from './hooks/useCurrency';
|
|
18
|
+
export { useDiscountQuery } from './hooks/useDiscountQuery';
|
|
18
19
|
export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
|
|
19
20
|
export { useOffersQuery as useOffers } from './hooks/useOffersQuery';
|
|
20
21
|
export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
|
|
@@ -28,6 +29,7 @@ export { useStoreConfigQuery as useStoreConfig } from './hooks/useStoreConfigQue
|
|
|
28
29
|
export { useThreeds } from './hooks/useThreeds';
|
|
29
30
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
30
31
|
export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
|
|
32
|
+
export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
|
|
31
33
|
export type { UseCheckoutTokenOptions, UseCheckoutTokenResult } from './hooks/useCheckoutToken';
|
|
32
34
|
export type { ExpressPaymentMethodsContextType, ExpressPaymentMethodsProviderProps } from './hooks/useExpressPaymentMethods';
|
|
33
35
|
export type { ApplePayButtonProps } from './components/ApplePayButton';
|
|
@@ -37,7 +39,9 @@ export type { ExtractedAddress, GooglePlaceDetails, GooglePrediction, UseGoogleA
|
|
|
37
39
|
export type { ISOCountry, ISORegion, UseISODataResult } from './hooks/useISOData';
|
|
38
40
|
export type { UsePluginConfigOptions, UsePluginConfigResult } from './hooks/usePluginConfig';
|
|
39
41
|
export type { UseCheckoutQueryOptions as UseCheckoutOptions, UseCheckoutQueryResult as UseCheckoutResult } from './hooks/useCheckoutQuery';
|
|
42
|
+
export type { StoreDiscount, UseDiscountQueryOptions as UseDiscountOptions, UseDiscountQueryResult as UseDiscountResult } from './hooks/useDiscountQuery';
|
|
40
43
|
export type { UseDiscountsQueryOptions as UseDiscountsOptions, UseDiscountsQueryResult as UseDiscountsResult } from './hooks/useDiscountsQuery';
|
|
44
|
+
export type { FunnelEvent, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext, UseFunnelOptions, UseFunnelResult } from './hooks/useFunnel';
|
|
41
45
|
export type { UseOffersQueryOptions as UseOffersOptions, UseOffersQueryResult as UseOffersResult } from './hooks/useOffersQuery';
|
|
42
46
|
export type { UseOrderBumpQueryOptions as UseOrderBumpOptions, UseOrderBumpQueryResult as UseOrderBumpResult } from './hooks/useOrderBumpQuery';
|
|
43
47
|
export type { UseOrderQueryOptions as UseOrderOptions, UseOrderQueryResult as UseOrderResult } from './hooks/useOrderQuery';
|
package/dist/v2/react/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { usePluginConfig } from './hooks/usePluginConfig';
|
|
|
19
19
|
export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
|
|
20
20
|
export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
|
|
21
21
|
export { useCurrency } from './hooks/useCurrency';
|
|
22
|
+
export { useDiscountQuery } from './hooks/useDiscountQuery';
|
|
22
23
|
export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
|
|
23
24
|
export { useOffersQuery as useOffers } from './hooks/useOffersQuery';
|
|
24
25
|
export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
|
|
@@ -32,5 +33,7 @@ export { useStoreConfigQuery as useStoreConfig } from './hooks/useStoreConfigQue
|
|
|
32
33
|
export { useThreeds } from './hooks/useThreeds';
|
|
33
34
|
export { useThreedsModal } from './hooks/useThreedsModal';
|
|
34
35
|
export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
|
|
36
|
+
// Funnel hooks
|
|
37
|
+
export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
|
|
35
38
|
// Re-export utilities from main react
|
|
36
39
|
export { formatMoney } from '../../react/utils/money';
|