@tagadapay/plugin-sdk 2.8.10 → 3.0.1
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/README.md +14 -14
- package/dist/index.js +1 -1
- package/dist/react/hooks/usePluginConfig.d.ts +1 -0
- package/dist/react/hooks/usePluginConfig.js +69 -18
- package/dist/react/providers/TagadaProvider.js +1 -4
- package/dist/v2/core/client.d.ts +18 -0
- package/dist/v2/core/client.js +45 -0
- package/dist/v2/core/config/environment.d.ts +8 -0
- package/dist/v2/core/config/environment.js +18 -0
- package/dist/v2/core/funnelClient.d.ts +84 -0
- package/dist/v2/core/funnelClient.js +252 -0
- package/dist/v2/core/index.d.ts +2 -0
- package/dist/v2/core/index.js +3 -0
- package/dist/v2/core/resources/apiClient.js +1 -1
- package/dist/v2/core/resources/funnel.d.ts +1 -0
- package/dist/v2/core/resources/offers.d.ts +182 -8
- package/dist/v2/core/resources/offers.js +25 -0
- package/dist/v2/core/resources/products.d.ts +5 -0
- package/dist/v2/core/resources/products.js +15 -1
- package/dist/v2/core/types.d.ts +1 -0
- package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
- package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
- package/dist/v2/core/utils/index.d.ts +2 -0
- package/dist/v2/core/utils/index.js +2 -0
- package/dist/v2/core/utils/pluginConfig.js +44 -32
- package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
- package/dist/v2/core/utils/sessionStorage.js +39 -0
- package/dist/v2/index.d.ts +3 -2
- package/dist/v2/index.js +1 -1
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
- package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
- package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
- package/dist/v2/react/hooks/useFunnel.d.ts +27 -39
- package/dist/v2/react/hooks/useFunnel.js +22 -659
- package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
- package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
- package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
- package/dist/v2/react/hooks/useOfferQuery.js +483 -0
- package/dist/v2/react/hooks/useOffersQuery.d.ts +9 -75
- package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
- package/dist/v2/react/hooks/useProductsQuery.js +10 -6
- package/dist/v2/react/index.d.ts +7 -4
- package/dist/v2/react/index.js +4 -2
- package/dist/v2/react/providers/TagadaProvider.d.ts +40 -2
- package/dist/v2/react/providers/TagadaProvider.js +116 -3
- package/dist/v2/standalone/index.d.ts +20 -0
- package/dist/v2/standalone/index.js +22 -0
- package/dist/v2/vue/index.d.ts +6 -0
- package/dist/v2/vue/index.js +10 -0
- package/package.json +6 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOffer Hook - Single offer workflow with checkout session management
|
|
3
|
+
*
|
|
4
|
+
* Behavior copied from useOffersQuery but simplified for a single offer:
|
|
5
|
+
* 1. Fetches offer data
|
|
6
|
+
* 2. Auto-initializes checkout session when mainOrderId is provided
|
|
7
|
+
* 3. Fetches order summary with variant options
|
|
8
|
+
* 4. Updates line items when variant/quantity changes
|
|
9
|
+
* 5. Pays with payOffer
|
|
10
|
+
*/
|
|
11
|
+
import { Offer } from '../../core/resources/offers';
|
|
12
|
+
export type { Offer };
|
|
13
|
+
/**
|
|
14
|
+
* Line item displayed in the UI
|
|
15
|
+
*/
|
|
16
|
+
export interface OfferLineItem {
|
|
17
|
+
id: string;
|
|
18
|
+
variantId: string;
|
|
19
|
+
variantName: string;
|
|
20
|
+
productId: string;
|
|
21
|
+
productName: string;
|
|
22
|
+
productDescription: string | null;
|
|
23
|
+
imageUrl: string | null;
|
|
24
|
+
quantity: number;
|
|
25
|
+
unitAmount: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Available variant option for a product
|
|
30
|
+
*/
|
|
31
|
+
export interface AvailableVariant {
|
|
32
|
+
variantId: string;
|
|
33
|
+
variantName: string;
|
|
34
|
+
sku: string | null;
|
|
35
|
+
imageUrl: string | null;
|
|
36
|
+
unitAmount: number;
|
|
37
|
+
currency: string;
|
|
38
|
+
isDefault: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* User selection for a line item
|
|
42
|
+
*/
|
|
43
|
+
export interface LineItemSelection {
|
|
44
|
+
variantId: string;
|
|
45
|
+
quantity: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Summary derived from offer with user selections
|
|
49
|
+
*/
|
|
50
|
+
export interface OfferPreviewSummary {
|
|
51
|
+
items: OfferLineItem[];
|
|
52
|
+
currency: string;
|
|
53
|
+
totalAmount: number;
|
|
54
|
+
totalAdjustedAmount: number;
|
|
55
|
+
}
|
|
56
|
+
export interface UseOfferQueryOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The offer ID to fetch (required)
|
|
59
|
+
*/
|
|
60
|
+
offerId: string;
|
|
61
|
+
/**
|
|
62
|
+
* Whether to fetch the offer automatically
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Main order ID - when provided, auto-inits checkout session
|
|
68
|
+
*/
|
|
69
|
+
mainOrderId?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface UseOfferQueryResult {
|
|
72
|
+
/** The fetched offer */
|
|
73
|
+
offer: Offer | null;
|
|
74
|
+
/** Loading state */
|
|
75
|
+
isLoading: boolean;
|
|
76
|
+
/** Whether order summary is being updated */
|
|
77
|
+
isUpdatingSummary: boolean;
|
|
78
|
+
/** Fetch error */
|
|
79
|
+
error: Error | null;
|
|
80
|
+
/** Preview summary with current selections */
|
|
81
|
+
summary: OfferPreviewSummary | null;
|
|
82
|
+
/** Line items with current selections */
|
|
83
|
+
lineItems: OfferLineItem[];
|
|
84
|
+
/** Get available variants for a product */
|
|
85
|
+
getAvailableVariants: (productId: string) => AvailableVariant[];
|
|
86
|
+
/** Select a variant for a product */
|
|
87
|
+
selectVariant: (productId: string, variantId: string) => void;
|
|
88
|
+
/** Update variant for a product (same as selectVariant) */
|
|
89
|
+
updateVariant: (productId: string, variantId: string) => void;
|
|
90
|
+
/** Update quantity for a product */
|
|
91
|
+
updateQuantity: (productId: string, quantity: number) => void;
|
|
92
|
+
/** Current selections per product */
|
|
93
|
+
selections: Record<string, LineItemSelection>;
|
|
94
|
+
/** Pay for the offer */
|
|
95
|
+
payOffer: (orderId?: string) => Promise<{
|
|
96
|
+
checkoutUrl: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** Payment in progress */
|
|
99
|
+
isPaying: boolean;
|
|
100
|
+
/** Payment error */
|
|
101
|
+
paymentError: Error | null;
|
|
102
|
+
/** Checkout session ID (set after init) */
|
|
103
|
+
checkoutSessionId: string | null;
|
|
104
|
+
/** Whether checkout session is initializing */
|
|
105
|
+
isInitializing: boolean;
|
|
106
|
+
/** Whether variant is loading for a product */
|
|
107
|
+
isLoadingVariant: (productId: string) => boolean;
|
|
108
|
+
}
|
|
109
|
+
export declare function useOfferQuery(options: UseOfferQueryOptions): UseOfferQueryResult;
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOffer Hook - Single offer workflow with checkout session management
|
|
3
|
+
*
|
|
4
|
+
* Behavior copied from useOffersQuery but simplified for a single offer:
|
|
5
|
+
* 1. Fetches offer data
|
|
6
|
+
* 2. Auto-initializes checkout session when mainOrderId is provided
|
|
7
|
+
* 3. Fetches order summary with variant options
|
|
8
|
+
* 4. Updates line items when variant/quantity changes
|
|
9
|
+
* 5. Pays with payOffer
|
|
10
|
+
*/
|
|
11
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { OffersResource } from '../../core/resources/offers';
|
|
14
|
+
import { useTagadaContext } from '../providers/TagadaProvider';
|
|
15
|
+
import { getGlobalApiClient } from './useApiQuery';
|
|
16
|
+
import { usePluginConfig } from './usePluginConfig';
|
|
17
|
+
export function useOfferQuery(options) {
|
|
18
|
+
const { offerId, enabled = true, mainOrderId } = options;
|
|
19
|
+
const { storeId } = usePluginConfig();
|
|
20
|
+
const { isSessionInitialized, session } = useTagadaContext();
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// State
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
const [checkoutSession, setCheckoutSession] = useState({
|
|
25
|
+
checkoutSessionId: null,
|
|
26
|
+
orderSummary: null,
|
|
27
|
+
selectedVariants: {},
|
|
28
|
+
loadingVariants: {},
|
|
29
|
+
isUpdatingSummary: false,
|
|
30
|
+
});
|
|
31
|
+
const [isInitializing, setIsInitializing] = useState(false);
|
|
32
|
+
const [isPaying, setIsPaying] = useState(false);
|
|
33
|
+
const [paymentError, setPaymentError] = useState(null);
|
|
34
|
+
// Use ref to break dependency cycles in callbacks
|
|
35
|
+
const checkoutSessionRef = useRef(checkoutSession);
|
|
36
|
+
checkoutSessionRef.current = checkoutSession;
|
|
37
|
+
// Track if we've already initialized to prevent duplicate calls
|
|
38
|
+
const hasInitializedRef = useRef(false);
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Initialize offers resource
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
const offersResource = useMemo(() => {
|
|
43
|
+
try {
|
|
44
|
+
return new OffersResource(getGlobalApiClient());
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error('Failed to initialize offers resource: ' +
|
|
48
|
+
(error instanceof Error ? error.message : 'Unknown error'));
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// 1. Fetch offer data
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
54
|
+
const { data: offer, isLoading: isOfferLoading, error, } = useQuery({
|
|
55
|
+
queryKey: ['offer', offerId, storeId],
|
|
56
|
+
queryFn: async () => {
|
|
57
|
+
if (!storeId) {
|
|
58
|
+
throw new Error('Store ID not found');
|
|
59
|
+
}
|
|
60
|
+
const offers = await offersResource.getOffersByIds(storeId, [offerId]);
|
|
61
|
+
return offers[0] || null;
|
|
62
|
+
},
|
|
63
|
+
enabled: enabled && !!offerId && !!storeId && isSessionInitialized,
|
|
64
|
+
staleTime: 5 * 60 * 1000,
|
|
65
|
+
refetchOnWindowFocus: false,
|
|
66
|
+
});
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Mutations
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
70
|
+
const { mutateAsync: initCheckoutSessionAsync } = useMutation({
|
|
71
|
+
mutationFn: async ({ offerId, orderId, customerId }) => {
|
|
72
|
+
return await offersResource.initCheckoutSession(offerId, orderId, customerId);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const { mutateAsync: payWithCheckoutSessionAsync } = useMutation({
|
|
76
|
+
mutationFn: async ({ checkoutSessionId, orderId }) => {
|
|
77
|
+
return await offersResource.payWithCheckoutSession(checkoutSessionId, orderId);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Helper: Fetch order summary from checkout session
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
83
|
+
const fetchOrderSummary = useCallback(async (sessionId) => {
|
|
84
|
+
if (!isSessionInitialized)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
setCheckoutSession(prev => ({
|
|
88
|
+
...prev,
|
|
89
|
+
isUpdatingSummary: true,
|
|
90
|
+
}));
|
|
91
|
+
const summaryResult = await offersResource.getOrderSummary(sessionId, true);
|
|
92
|
+
if (summaryResult) {
|
|
93
|
+
// Sort items by productId to ensure consistent order
|
|
94
|
+
const sortedItems = [...summaryResult.items].sort((a, b) => a.productId.localeCompare(b.productId));
|
|
95
|
+
const orderSummary = {
|
|
96
|
+
...summaryResult,
|
|
97
|
+
items: sortedItems,
|
|
98
|
+
};
|
|
99
|
+
// Initialize selected variants based on the summary
|
|
100
|
+
const initialVariants = {};
|
|
101
|
+
sortedItems.forEach((item) => {
|
|
102
|
+
if (item.productId && item.variantId) {
|
|
103
|
+
initialVariants[item.productId] = item.variantId;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
setCheckoutSession(prev => ({
|
|
107
|
+
...prev,
|
|
108
|
+
orderSummary,
|
|
109
|
+
selectedVariants: initialVariants,
|
|
110
|
+
isUpdatingSummary: false,
|
|
111
|
+
}));
|
|
112
|
+
return orderSummary;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Failed to fetch order summary:', error);
|
|
118
|
+
setCheckoutSession(prev => ({
|
|
119
|
+
...prev,
|
|
120
|
+
isUpdatingSummary: false,
|
|
121
|
+
}));
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}, [offersResource, isSessionInitialized]);
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// Helper: Initialize checkout session
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
128
|
+
const initCheckoutSession = useCallback(async (offerId, orderId, customerId) => {
|
|
129
|
+
if (!isSessionInitialized) {
|
|
130
|
+
throw new Error('Cannot initialize checkout session: CMS session is not initialized');
|
|
131
|
+
}
|
|
132
|
+
const effectiveCustomerId = customerId || session?.customerId;
|
|
133
|
+
if (!effectiveCustomerId) {
|
|
134
|
+
throw new Error('Customer ID is required. Make sure the session is properly initialized.');
|
|
135
|
+
}
|
|
136
|
+
return await initCheckoutSessionAsync({
|
|
137
|
+
offerId,
|
|
138
|
+
orderId,
|
|
139
|
+
customerId: effectiveCustomerId,
|
|
140
|
+
});
|
|
141
|
+
}, [initCheckoutSessionAsync, session?.customerId, isSessionInitialized]);
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
143
|
+
// 2. Auto-initialize checkout session when mainOrderId is provided
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!offer || !mainOrderId || !isSessionInitialized)
|
|
147
|
+
return;
|
|
148
|
+
if (checkoutSession.checkoutSessionId || isInitializing || hasInitializedRef.current)
|
|
149
|
+
return;
|
|
150
|
+
const initSession = async () => {
|
|
151
|
+
hasInitializedRef.current = true;
|
|
152
|
+
setIsInitializing(true);
|
|
153
|
+
try {
|
|
154
|
+
const result = await initCheckoutSession(offerId, mainOrderId);
|
|
155
|
+
setCheckoutSession(prev => ({
|
|
156
|
+
...prev,
|
|
157
|
+
checkoutSessionId: result.checkoutSessionId,
|
|
158
|
+
}));
|
|
159
|
+
// Fetch order summary after session is initialized
|
|
160
|
+
await fetchOrderSummary(result.checkoutSessionId);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error('Failed to init checkout session:', err);
|
|
164
|
+
hasInitializedRef.current = false; // Allow retry on error
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
setIsInitializing(false);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
initSession();
|
|
171
|
+
}, [offer, mainOrderId, offerId, isSessionInitialized, checkoutSession.checkoutSessionId, isInitializing, initCheckoutSession, fetchOrderSummary]);
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
173
|
+
// Derive line items from order summary or static offer data
|
|
174
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
175
|
+
const lineItems = useMemo(() => {
|
|
176
|
+
// Prefer dynamic data from order summary
|
|
177
|
+
if (checkoutSession.orderSummary?.items) {
|
|
178
|
+
return checkoutSession.orderSummary.items.map((item) => ({
|
|
179
|
+
id: item.id || `${item.productId}-${item.variantId}`,
|
|
180
|
+
variantId: item.variantId,
|
|
181
|
+
variantName: item.variant?.name || item.variantName || '',
|
|
182
|
+
productId: item.productId,
|
|
183
|
+
productName: item.product?.name || item.productName || '',
|
|
184
|
+
productDescription: item.product?.description || null,
|
|
185
|
+
imageUrl: item.variant?.imageUrl || item.imageUrl || null,
|
|
186
|
+
quantity: item.quantity,
|
|
187
|
+
unitAmount: item.unitAmount || 0,
|
|
188
|
+
currency: checkoutSession.orderSummary?.currency || 'USD',
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
// Fallback to static offer data
|
|
192
|
+
if (!offer?.offerLineItems)
|
|
193
|
+
return [];
|
|
194
|
+
const items = [];
|
|
195
|
+
for (const item of offer.offerLineItems) {
|
|
196
|
+
const price = item.price;
|
|
197
|
+
const variant = price?.variant;
|
|
198
|
+
const product = variant?.product;
|
|
199
|
+
if (!variant || !product)
|
|
200
|
+
continue;
|
|
201
|
+
const currency = offer.summaries?.[0]?.currency || 'USD';
|
|
202
|
+
const currencyOption = price?.currencyOptions?.[currency];
|
|
203
|
+
const unitAmount = currencyOption?.amount ?? variant.price ?? 0;
|
|
204
|
+
items.push({
|
|
205
|
+
id: item.id,
|
|
206
|
+
variantId: variant.id,
|
|
207
|
+
variantName: variant.name,
|
|
208
|
+
productId: product.id,
|
|
209
|
+
productName: product.name,
|
|
210
|
+
productDescription: product.description,
|
|
211
|
+
imageUrl: variant.imageUrl,
|
|
212
|
+
quantity: item.quantity,
|
|
213
|
+
unitAmount,
|
|
214
|
+
currency,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return items;
|
|
218
|
+
}, [offer, checkoutSession.orderSummary]);
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
220
|
+
// Derive selections from order summary or line items
|
|
221
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
222
|
+
const selections = useMemo(() => {
|
|
223
|
+
if (Object.keys(checkoutSession.selectedVariants).length > 0) {
|
|
224
|
+
const result = {};
|
|
225
|
+
for (const item of lineItems) {
|
|
226
|
+
result[item.productId] = {
|
|
227
|
+
variantId: checkoutSession.selectedVariants[item.productId] || item.variantId,
|
|
228
|
+
quantity: item.quantity,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
// Fallback to line items
|
|
234
|
+
const result = {};
|
|
235
|
+
for (const item of lineItems) {
|
|
236
|
+
if (!result[item.productId]) {
|
|
237
|
+
result[item.productId] = {
|
|
238
|
+
variantId: item.variantId,
|
|
239
|
+
quantity: item.quantity,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}, [lineItems, checkoutSession.selectedVariants]);
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
246
|
+
// Summary calculation
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
248
|
+
const summary = useMemo(() => {
|
|
249
|
+
// Prefer dynamic data from order summary
|
|
250
|
+
if (checkoutSession.orderSummary) {
|
|
251
|
+
return {
|
|
252
|
+
items: lineItems,
|
|
253
|
+
currency: checkoutSession.orderSummary.currency || 'USD',
|
|
254
|
+
totalAmount: checkoutSession.orderSummary.totalAmount || 0,
|
|
255
|
+
totalAdjustedAmount: checkoutSession.orderSummary.totalAdjustedAmount || 0,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Fallback to static calculation
|
|
259
|
+
if (lineItems.length === 0)
|
|
260
|
+
return null;
|
|
261
|
+
const currency = lineItems[0]?.currency || 'USD';
|
|
262
|
+
let totalAmount = 0;
|
|
263
|
+
for (const item of lineItems) {
|
|
264
|
+
totalAmount += item.unitAmount * item.quantity;
|
|
265
|
+
}
|
|
266
|
+
const offerSummary = offer?.summaries?.[0];
|
|
267
|
+
const discountRatio = offerSummary && offerSummary.totalAmount > 0
|
|
268
|
+
? offerSummary.totalAdjustedAmount / offerSummary.totalAmount
|
|
269
|
+
: 1;
|
|
270
|
+
return {
|
|
271
|
+
items: lineItems,
|
|
272
|
+
currency,
|
|
273
|
+
totalAmount,
|
|
274
|
+
totalAdjustedAmount: Math.round(totalAmount * discountRatio),
|
|
275
|
+
};
|
|
276
|
+
}, [lineItems, offer, checkoutSession.orderSummary]);
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
278
|
+
// Get available variants for a product
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
280
|
+
const getAvailableVariants = useCallback((productId) => {
|
|
281
|
+
// Get variants from order summary options (dynamic, from checkout session)
|
|
282
|
+
const orderSummary = checkoutSession.orderSummary;
|
|
283
|
+
if (orderSummary?.options?.[productId]) {
|
|
284
|
+
const currency = orderSummary.currency || 'USD';
|
|
285
|
+
return orderSummary.options[productId].map((variant) => ({
|
|
286
|
+
variantId: variant.id,
|
|
287
|
+
variantName: variant.name,
|
|
288
|
+
sku: variant.sku || null,
|
|
289
|
+
imageUrl: variant.imageUrl || null,
|
|
290
|
+
unitAmount: variant.prices?.[0]?.currencyOptions?.[currency]?.amount ?? 0,
|
|
291
|
+
currency,
|
|
292
|
+
isDefault: variant.default ?? false,
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
// Fallback to static offer data
|
|
296
|
+
if (!offer?.offerLineItems)
|
|
297
|
+
return [];
|
|
298
|
+
const variants = [];
|
|
299
|
+
const currency = offer.summaries?.[0]?.currency || 'USD';
|
|
300
|
+
for (const item of offer.offerLineItems) {
|
|
301
|
+
const price = item.price;
|
|
302
|
+
const variant = price?.variant;
|
|
303
|
+
const product = variant?.product;
|
|
304
|
+
if (!variant || !product || product.id !== productId)
|
|
305
|
+
continue;
|
|
306
|
+
if (variants.some(v => v.variantId === variant.id))
|
|
307
|
+
continue;
|
|
308
|
+
const currencyOption = price?.currencyOptions?.[currency];
|
|
309
|
+
const unitAmount = currencyOption?.amount ?? variant.price ?? 0;
|
|
310
|
+
variants.push({
|
|
311
|
+
variantId: variant.id,
|
|
312
|
+
variantName: variant.name,
|
|
313
|
+
sku: variant.sku || null,
|
|
314
|
+
imageUrl: variant.imageUrl,
|
|
315
|
+
unitAmount,
|
|
316
|
+
currency,
|
|
317
|
+
isDefault: variant.default ?? false,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return variants;
|
|
321
|
+
}, [checkoutSession.orderSummary, offer]);
|
|
322
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
323
|
+
// Check if variant is loading
|
|
324
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
325
|
+
const isLoadingVariant = useCallback((productId) => {
|
|
326
|
+
return checkoutSession.loadingVariants[productId] ?? false;
|
|
327
|
+
}, [checkoutSession.loadingVariants]);
|
|
328
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
329
|
+
// Select/Update variant for a product
|
|
330
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
331
|
+
const selectVariant = useCallback(async (productId, variantId) => {
|
|
332
|
+
if (!isSessionInitialized) {
|
|
333
|
+
throw new Error('Cannot select variant: CMS session is not initialized');
|
|
334
|
+
}
|
|
335
|
+
const currentSession = checkoutSessionRef.current;
|
|
336
|
+
if (!currentSession.checkoutSessionId || !currentSession.orderSummary) {
|
|
337
|
+
throw new Error('Checkout session not initialized');
|
|
338
|
+
}
|
|
339
|
+
const sessionId = currentSession.checkoutSessionId;
|
|
340
|
+
// Set loading state
|
|
341
|
+
setCheckoutSession(prev => ({
|
|
342
|
+
...prev,
|
|
343
|
+
loadingVariants: {
|
|
344
|
+
...prev.loadingVariants,
|
|
345
|
+
[productId]: true,
|
|
346
|
+
},
|
|
347
|
+
}));
|
|
348
|
+
try {
|
|
349
|
+
// Find the current item to get its quantity
|
|
350
|
+
const currentItem = currentSession.orderSummary.items.find((item) => item.productId === productId);
|
|
351
|
+
if (!currentItem) {
|
|
352
|
+
throw new Error('Current item not found');
|
|
353
|
+
}
|
|
354
|
+
// Update selected variants state optimistically
|
|
355
|
+
setCheckoutSession(prev => ({
|
|
356
|
+
...prev,
|
|
357
|
+
selectedVariants: {
|
|
358
|
+
...prev.selectedVariants,
|
|
359
|
+
[productId]: variantId,
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
362
|
+
// Update line items on the server
|
|
363
|
+
await offersResource.updateLineItems(sessionId, [
|
|
364
|
+
{
|
|
365
|
+
variantId,
|
|
366
|
+
quantity: currentItem.quantity,
|
|
367
|
+
},
|
|
368
|
+
]);
|
|
369
|
+
// Refetch order summary
|
|
370
|
+
await fetchOrderSummary(sessionId);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
setCheckoutSession(prev => ({
|
|
374
|
+
...prev,
|
|
375
|
+
loadingVariants: {
|
|
376
|
+
...prev.loadingVariants,
|
|
377
|
+
[productId]: false,
|
|
378
|
+
},
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
}, [offersResource, fetchOrderSummary, isSessionInitialized]);
|
|
382
|
+
// Alias for selectVariant
|
|
383
|
+
const updateVariant = selectVariant;
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
385
|
+
// Update quantity for a product
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
387
|
+
const updateQuantity = useCallback(async (productId, quantity) => {
|
|
388
|
+
if (quantity < 1)
|
|
389
|
+
return;
|
|
390
|
+
if (!isSessionInitialized) {
|
|
391
|
+
throw new Error('Cannot update quantity: CMS session is not initialized');
|
|
392
|
+
}
|
|
393
|
+
const currentSession = checkoutSessionRef.current;
|
|
394
|
+
if (!currentSession.checkoutSessionId || !currentSession.orderSummary) {
|
|
395
|
+
throw new Error('Checkout session not initialized');
|
|
396
|
+
}
|
|
397
|
+
const sessionId = currentSession.checkoutSessionId;
|
|
398
|
+
// Find the current item
|
|
399
|
+
const currentItem = currentSession.orderSummary.items.find((item) => item.productId === productId);
|
|
400
|
+
if (!currentItem) {
|
|
401
|
+
throw new Error('Current item not found');
|
|
402
|
+
}
|
|
403
|
+
// Set loading state
|
|
404
|
+
setCheckoutSession(prev => ({
|
|
405
|
+
...prev,
|
|
406
|
+
loadingVariants: {
|
|
407
|
+
...prev.loadingVariants,
|
|
408
|
+
[productId]: true,
|
|
409
|
+
},
|
|
410
|
+
}));
|
|
411
|
+
try {
|
|
412
|
+
// Update line items on the server
|
|
413
|
+
await offersResource.updateLineItems(sessionId, [
|
|
414
|
+
{
|
|
415
|
+
variantId: currentItem.variantId,
|
|
416
|
+
quantity,
|
|
417
|
+
},
|
|
418
|
+
]);
|
|
419
|
+
// Refetch order summary
|
|
420
|
+
await fetchOrderSummary(sessionId);
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
setCheckoutSession(prev => ({
|
|
424
|
+
...prev,
|
|
425
|
+
loadingVariants: {
|
|
426
|
+
...prev.loadingVariants,
|
|
427
|
+
[productId]: false,
|
|
428
|
+
},
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
}, [offersResource, fetchOrderSummary, isSessionInitialized]);
|
|
432
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
433
|
+
// Pay for the offer
|
|
434
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
435
|
+
const payOffer = useCallback(async (orderId) => {
|
|
436
|
+
if (!isSessionInitialized) {
|
|
437
|
+
throw new Error('Cannot pay offer: CMS session is not initialized');
|
|
438
|
+
}
|
|
439
|
+
const currentSession = checkoutSessionRef.current;
|
|
440
|
+
if (!currentSession.checkoutSessionId) {
|
|
441
|
+
throw new Error('Checkout session not initialized');
|
|
442
|
+
}
|
|
443
|
+
setIsPaying(true);
|
|
444
|
+
setPaymentError(null);
|
|
445
|
+
try {
|
|
446
|
+
await payWithCheckoutSessionAsync({
|
|
447
|
+
checkoutSessionId: currentSession.checkoutSessionId,
|
|
448
|
+
orderId: orderId || mainOrderId,
|
|
449
|
+
});
|
|
450
|
+
return { checkoutUrl: '' };
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
const error = err instanceof Error ? err : new Error('Payment failed');
|
|
454
|
+
setPaymentError(error);
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
setIsPaying(false);
|
|
459
|
+
}
|
|
460
|
+
}, [mainOrderId, payWithCheckoutSessionAsync, isSessionInitialized]);
|
|
461
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
462
|
+
// Return
|
|
463
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
464
|
+
return {
|
|
465
|
+
offer: offer || null,
|
|
466
|
+
isLoading: isOfferLoading,
|
|
467
|
+
isUpdatingSummary: checkoutSession.isUpdatingSummary,
|
|
468
|
+
error: error,
|
|
469
|
+
summary,
|
|
470
|
+
lineItems,
|
|
471
|
+
getAvailableVariants,
|
|
472
|
+
selectVariant,
|
|
473
|
+
updateVariant,
|
|
474
|
+
updateQuantity,
|
|
475
|
+
selections,
|
|
476
|
+
payOffer,
|
|
477
|
+
isPaying,
|
|
478
|
+
paymentError,
|
|
479
|
+
checkoutSessionId: checkoutSession.checkoutSessionId,
|
|
480
|
+
isInitializing,
|
|
481
|
+
isLoadingVariant,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
@@ -1,78 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Handles offers with automatic caching
|
|
4
|
-
*/
|
|
5
|
-
import { Offer, OfferSummary } from '../../core/resources/offers';
|
|
6
|
-
import { CurrencyOptions, OrderSummary } from '../../core/resources/postPurchases';
|
|
7
|
-
export interface UseOffersQueryOptions {
|
|
8
|
-
/**
|
|
9
|
-
* Array of offer IDs to fetch
|
|
10
|
-
*/
|
|
11
|
-
offerIds?: string[];
|
|
12
|
-
/**
|
|
13
|
-
* Whether to fetch offers automatically on mount
|
|
14
|
-
* @default true
|
|
15
|
-
*/
|
|
16
|
-
enabled?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Return URL for checkout sessions
|
|
19
|
-
*/
|
|
20
|
-
returnUrl?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Order ID to associate with the offers (required for payments)
|
|
23
|
-
*/
|
|
24
|
-
orderId?: string;
|
|
25
|
-
/**
|
|
26
|
-
* The ID of the currently active offer to preview/fetch summary for.
|
|
27
|
-
* If provided, the hook will automatically fetch and manage the summary for this offer.
|
|
28
|
-
*/
|
|
29
|
-
activeOfferId?: string;
|
|
30
|
-
/**
|
|
31
|
-
* Whether to skip auto-preview fetching (e.g. during navigation or processing)
|
|
32
|
-
*/
|
|
33
|
-
skipPreview?: boolean;
|
|
34
|
-
}
|
|
35
|
-
export interface UseOffersQueryResult {
|
|
36
|
-
offers: Offer[];
|
|
1
|
+
export function useOffersQuery(options?: {}): {
|
|
2
|
+
offers: import("../../core/resources/offers").Offer[];
|
|
37
3
|
isLoading: boolean;
|
|
38
4
|
error: Error | null;
|
|
39
|
-
|
|
40
|
-
* Summary for the active offer (if activeOfferId is provided)
|
|
41
|
-
* Automatically falls back to static summary while loading dynamic data
|
|
42
|
-
*/
|
|
43
|
-
activeSummary: OrderSummary | OfferSummary | null;
|
|
44
|
-
/**
|
|
45
|
-
* Whether the active offer summary is currently loading
|
|
46
|
-
*/
|
|
5
|
+
activeSummary: any;
|
|
47
6
|
isActiveSummaryLoading: boolean;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* Preview an offer's price/summary
|
|
55
|
-
*/
|
|
56
|
-
preview: (offerId: string) => Promise<OrderSummary | OfferSummary | null>;
|
|
57
|
-
/**
|
|
58
|
-
* Get available variants for a product in an offer
|
|
59
|
-
*/
|
|
60
|
-
getAvailableVariants: (offerId: string, productId: string) => {
|
|
61
|
-
variantId: string;
|
|
62
|
-
variantName: string;
|
|
63
|
-
variantSku: string | null;
|
|
64
|
-
variantDefault: boolean | null;
|
|
65
|
-
variantExternalId: string | null;
|
|
66
|
-
priceId: string;
|
|
67
|
-
currencyOptions: CurrencyOptions;
|
|
68
|
-
}[];
|
|
69
|
-
/**
|
|
70
|
-
* Select a variant for a product in an offer
|
|
71
|
-
*/
|
|
72
|
-
selectVariant: (offerId: string, productId: string, variantId: string) => Promise<OrderSummary | null>;
|
|
73
|
-
/**
|
|
74
|
-
* Check if variants are loading for a product
|
|
75
|
-
*/
|
|
76
|
-
isLoadingVariants: (offerId: string, productId: string) => boolean;
|
|
77
|
-
}
|
|
78
|
-
export declare function useOffersQuery(options?: UseOffersQueryOptions): UseOffersQueryResult;
|
|
7
|
+
payOffer: (offerId: any, orderId: any) => Promise<void>;
|
|
8
|
+
preview: (offerId: any) => Promise<any>;
|
|
9
|
+
getAvailableVariants: (offerId: any, productId: any) => any;
|
|
10
|
+
selectVariant: (offerId: any, productId: any, variantId: any) => Promise<any>;
|
|
11
|
+
isLoadingVariants: (offerId: any, productId: any) => any;
|
|
12
|
+
};
|