@decocms/apps 1.6.5 → 1.7.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/package.json
CHANGED
|
@@ -37,25 +37,41 @@ export type CartFragment = {
|
|
|
37
37
|
|
|
38
38
|
// Mutation types
|
|
39
39
|
export type AddItemToCartMutation = { cart?: CartFragment | null };
|
|
40
|
-
export type AddItemToCartMutationVariables = { cartId: string; lines:
|
|
40
|
+
export type AddItemToCartMutationVariables = { cartId: string; lines: unknown };
|
|
41
41
|
export type UpdateItemsMutation = { cart?: CartFragment | null };
|
|
42
|
-
export type UpdateItemsMutationVariables = { cartId: string; lines:
|
|
42
|
+
export type UpdateItemsMutationVariables = { cartId: string; lines: unknown };
|
|
43
43
|
export type AddCouponMutation = { cart?: CartFragment | null };
|
|
44
44
|
export type AddCouponMutationVariables = { cartId: string; discountCodes: string[] };
|
|
45
45
|
|
|
46
|
-
// Product types
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export type
|
|
51
|
-
export type
|
|
46
|
+
// Product types — these are intentionally loose stubs because the
|
|
47
|
+
// real Shopify Storefront API GraphQL types are huge and only a tiny
|
|
48
|
+
// subset is consumed. `unknown` keeps consumers honest (forces a cast
|
|
49
|
+
// at the boundary) without exploding the type surface.
|
|
50
|
+
export type ProductFragment = unknown;
|
|
51
|
+
export type ProductVariantFragment = unknown;
|
|
52
|
+
export type GetProductQuery = { product?: unknown };
|
|
53
|
+
export type GetProductQueryVariables = { handle?: string; identifiers?: unknown[] };
|
|
54
|
+
export type ProductRecommendationsQuery = { productRecommendations?: unknown[] };
|
|
52
55
|
export type ProductRecommendationsQueryVariables = { productId: string };
|
|
53
56
|
|
|
54
57
|
// Search/Collection types
|
|
55
58
|
export type InputMaybe<T> = T | null | undefined;
|
|
56
59
|
export type ProductCollectionSortKeys = string;
|
|
57
60
|
export type SearchSortKeys = string;
|
|
58
|
-
|
|
61
|
+
// Loose shape derived from the only consumers in
|
|
62
|
+
// `shopify/utils/utils.ts` (filterToObject + getFiltersByUrl). Keeps
|
|
63
|
+
// the types honest without depending on Shopify's full GraphQL schema.
|
|
64
|
+
export type ProductFilter = {
|
|
65
|
+
tag?: string;
|
|
66
|
+
productType?: string;
|
|
67
|
+
productVendor?: string;
|
|
68
|
+
available?: boolean;
|
|
69
|
+
price?: { min?: number; max?: number };
|
|
70
|
+
variantOption?: { name: string; value: string };
|
|
71
|
+
productMetafield?: { namespace: string; key: string; value: string };
|
|
72
|
+
taxonomyMetafield?: { namespace: string; key: string; value: string };
|
|
73
|
+
category?: { id: string };
|
|
74
|
+
};
|
|
59
75
|
|
|
60
76
|
// Customer types
|
|
61
77
|
export type Customer = {
|
|
@@ -65,9 +81,9 @@ export type Customer = {
|
|
|
65
81
|
email?: string | null;
|
|
66
82
|
phone?: string | null;
|
|
67
83
|
acceptsMarketing?: boolean;
|
|
68
|
-
defaultAddress?:
|
|
69
|
-
addresses?: { nodes:
|
|
70
|
-
orders?: { nodes:
|
|
84
|
+
defaultAddress?: unknown;
|
|
85
|
+
addresses?: { nodes: unknown[] };
|
|
86
|
+
orders?: { nodes: unknown[] };
|
|
71
87
|
};
|
|
72
88
|
|
|
73
89
|
export type CustomerAccessTokenCreateInput = {
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for the legacy invoke-based `useCart` hook.
|
|
3
|
+
*
|
|
4
|
+
* This is the API shape that migrated Fresh sites depend on:
|
|
5
|
+
* - module-level singleton state (no QueryClient required)
|
|
6
|
+
* - listener-based re-render (`forceRender` on a useState counter)
|
|
7
|
+
* - awaitable async actions (`await addItem(...)`) instead of TanStack mutations
|
|
8
|
+
* - signal-shaped accessors (`cart.value`, `cart.value = ...`)
|
|
9
|
+
*
|
|
10
|
+
* It is intentionally separate from the canonical `useCart` in
|
|
11
|
+
* `vtex/hooks/useCart.ts`, which is built on TanStack Query and exposes the
|
|
12
|
+
* `Minicart` shape. Both can coexist in a single site.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // src/hooks/useCart.ts
|
|
17
|
+
* import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart";
|
|
18
|
+
* import { invoke } from "~/server/invoke";
|
|
19
|
+
*
|
|
20
|
+
* export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart({
|
|
21
|
+
* invoke,
|
|
22
|
+
* });
|
|
23
|
+
* export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useEffect, useState } from "react";
|
|
28
|
+
import type { OrderForm, OrderFormItem } from "../types";
|
|
29
|
+
|
|
30
|
+
/** Minimal structural shape of the invoke proxy this hook needs. */
|
|
31
|
+
export interface CreateUseCartInvoke {
|
|
32
|
+
vtex: {
|
|
33
|
+
actions: {
|
|
34
|
+
getOrCreateCart: (args: { data: { orderFormId?: string } }) => Promise<OrderForm>;
|
|
35
|
+
addItemsToCart: (args: {
|
|
36
|
+
data: {
|
|
37
|
+
orderFormId: string;
|
|
38
|
+
orderItems: Array<{ id: string; seller: string; quantity: number }>;
|
|
39
|
+
};
|
|
40
|
+
}) => Promise<OrderForm>;
|
|
41
|
+
updateCartItems: (args: {
|
|
42
|
+
data: {
|
|
43
|
+
orderFormId: string;
|
|
44
|
+
orderItems: Array<{ index: number; quantity: number }>;
|
|
45
|
+
};
|
|
46
|
+
}) => Promise<OrderForm>;
|
|
47
|
+
addCouponToCart: (args: {
|
|
48
|
+
data: { orderFormId: string; text: string };
|
|
49
|
+
}) => Promise<OrderForm>;
|
|
50
|
+
updateOrderFormAttachment: (args: {
|
|
51
|
+
data: {
|
|
52
|
+
orderFormId: string;
|
|
53
|
+
attachment: string;
|
|
54
|
+
body: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}) => Promise<OrderForm>;
|
|
57
|
+
simulateCart: (args: {
|
|
58
|
+
data: {
|
|
59
|
+
items: Array<{ id: string; quantity: number; seller: string }>;
|
|
60
|
+
postalCode: string;
|
|
61
|
+
country: string;
|
|
62
|
+
};
|
|
63
|
+
}) => Promise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CreateUseCartOptions {
|
|
69
|
+
invoke: CreateUseCartInvoke;
|
|
70
|
+
/**
|
|
71
|
+
* Override the orderFormId cookie name. VTEX standard is
|
|
72
|
+
* `checkout.vtex.com__orderFormId`, which is the default.
|
|
73
|
+
*/
|
|
74
|
+
orderFormCookieName?: string;
|
|
75
|
+
/** Override the cookie max-age in seconds. Default: 7 days. */
|
|
76
|
+
orderFormCookieMaxAge?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build a per-site `useCart` plus its companions. */
|
|
80
|
+
export function createUseCart(opts: CreateUseCartOptions) {
|
|
81
|
+
const { invoke } = opts;
|
|
82
|
+
const COOKIE_NAME = opts.orderFormCookieName ?? "checkout.vtex.com__orderFormId";
|
|
83
|
+
const COOKIE_MAX_AGE = opts.orderFormCookieMaxAge ?? 7 * 24 * 3600;
|
|
84
|
+
|
|
85
|
+
let _orderForm: OrderForm | null = null;
|
|
86
|
+
let _loading = false;
|
|
87
|
+
let _initStarted = false;
|
|
88
|
+
let _initFailed = false;
|
|
89
|
+
const _listeners = new Set<() => void>();
|
|
90
|
+
|
|
91
|
+
function notify() {
|
|
92
|
+
for (const fn of _listeners) fn();
|
|
93
|
+
}
|
|
94
|
+
function setOrderForm(of: OrderForm | null) {
|
|
95
|
+
_orderForm = of;
|
|
96
|
+
notify();
|
|
97
|
+
}
|
|
98
|
+
function setLoading(v: boolean) {
|
|
99
|
+
_loading = v;
|
|
100
|
+
notify();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapeRegex(s: string): string {
|
|
104
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getOrderFormIdFromCookie(): string | null {
|
|
108
|
+
if (typeof document === "undefined") return null;
|
|
109
|
+
const re = new RegExp(`${escapeRegex(COOKIE_NAME)}=([^;]*)`);
|
|
110
|
+
const match = document.cookie.match(re);
|
|
111
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setOrderFormIdCookie(id: string) {
|
|
115
|
+
if (typeof document === "undefined") return;
|
|
116
|
+
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(id)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function ensureOrderForm(): Promise<string> {
|
|
120
|
+
if (_orderForm?.orderFormId) return _orderForm.orderFormId;
|
|
121
|
+
|
|
122
|
+
const existing = getOrderFormIdFromCookie();
|
|
123
|
+
const of = await invoke.vtex.actions.getOrCreateCart({
|
|
124
|
+
data: { orderFormId: existing || undefined },
|
|
125
|
+
});
|
|
126
|
+
setOrderForm(of);
|
|
127
|
+
if (of?.orderFormId) setOrderFormIdCookie(of.orderFormId);
|
|
128
|
+
return of.orderFormId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function itemToAnalyticsItem(item: OrderFormItem & { coupon?: string }, index: number) {
|
|
132
|
+
return {
|
|
133
|
+
item_id: item.productId,
|
|
134
|
+
item_group_id: item.productId,
|
|
135
|
+
item_name: item.name ?? item.skuName ?? "",
|
|
136
|
+
item_variant: item.skuName,
|
|
137
|
+
item_brand: item.additionalInfo?.brandName ?? "",
|
|
138
|
+
price: (item.sellingPrice ?? item.price ?? 0) / 100,
|
|
139
|
+
discount: Number(((item.listPrice - item.sellingPrice) / 100).toFixed(2)),
|
|
140
|
+
quantity: item.quantity,
|
|
141
|
+
coupon: item.coupon,
|
|
142
|
+
affiliation: item.seller,
|
|
143
|
+
index,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reset all module-level cart state so the next useCart() re-fetches. */
|
|
148
|
+
function resetCart() {
|
|
149
|
+
_orderForm = null;
|
|
150
|
+
_loading = false;
|
|
151
|
+
_initStarted = false;
|
|
152
|
+
_initFailed = false;
|
|
153
|
+
notify();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useCart() {
|
|
157
|
+
const [, forceRender] = useState(0);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const listener = () => forceRender((n) => n + 1);
|
|
161
|
+
_listeners.add(listener);
|
|
162
|
+
|
|
163
|
+
if (!_orderForm && !_initStarted) {
|
|
164
|
+
_initStarted = true;
|
|
165
|
+
const ofId = getOrderFormIdFromCookie();
|
|
166
|
+
setLoading(true);
|
|
167
|
+
invoke.vtex.actions
|
|
168
|
+
.getOrCreateCart({ data: { orderFormId: ofId || undefined } })
|
|
169
|
+
.then((of) => {
|
|
170
|
+
setOrderForm(of);
|
|
171
|
+
if (of?.orderFormId) setOrderFormIdCookie(of.orderFormId);
|
|
172
|
+
})
|
|
173
|
+
.catch((err: unknown) => {
|
|
174
|
+
console.error("[useCart] init failed:", err);
|
|
175
|
+
// Keep previous orderForm if we had one (e.g. after SPA navigation)
|
|
176
|
+
// so user data isn't lost on transient VTEX 503s.
|
|
177
|
+
if (!_orderForm) {
|
|
178
|
+
_initFailed = true;
|
|
179
|
+
notify();
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
.finally(() => setLoading(false));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return () => {
|
|
186
|
+
_listeners.delete(listener);
|
|
187
|
+
};
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
cart: {
|
|
192
|
+
get value() {
|
|
193
|
+
return _orderForm;
|
|
194
|
+
},
|
|
195
|
+
set value(v: OrderForm | null) {
|
|
196
|
+
setOrderForm(v);
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
loading: {
|
|
201
|
+
get value() {
|
|
202
|
+
return _loading;
|
|
203
|
+
},
|
|
204
|
+
set value(v: boolean) {
|
|
205
|
+
setLoading(v);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
initFailed: {
|
|
210
|
+
get value() {
|
|
211
|
+
return _initFailed;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
addItem: async (params: { id: string; seller: string; quantity?: number }) => {
|
|
216
|
+
setLoading(true);
|
|
217
|
+
try {
|
|
218
|
+
const ofId = await ensureOrderForm();
|
|
219
|
+
const updated = await invoke.vtex.actions.addItemsToCart({
|
|
220
|
+
data: {
|
|
221
|
+
orderFormId: ofId,
|
|
222
|
+
orderItems: [
|
|
223
|
+
{
|
|
224
|
+
id: params.id,
|
|
225
|
+
seller: params.seller,
|
|
226
|
+
quantity: params.quantity ?? 1,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
setOrderForm(updated);
|
|
232
|
+
if (updated?.orderFormId) setOrderFormIdCookie(updated.orderFormId);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error("[useCart] addItem failed:", err);
|
|
235
|
+
throw err;
|
|
236
|
+
} finally {
|
|
237
|
+
setLoading(false);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
addItems: async (params: {
|
|
242
|
+
orderItems: Array<{ id: string; seller: string; quantity: number }>;
|
|
243
|
+
}) => {
|
|
244
|
+
setLoading(true);
|
|
245
|
+
try {
|
|
246
|
+
const ofId = await ensureOrderForm();
|
|
247
|
+
const updated = await invoke.vtex.actions.addItemsToCart({
|
|
248
|
+
data: { orderFormId: ofId, orderItems: params.orderItems },
|
|
249
|
+
});
|
|
250
|
+
setOrderForm(updated);
|
|
251
|
+
if (updated?.orderFormId) setOrderFormIdCookie(updated.orderFormId);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error("[useCart] addItems failed:", err);
|
|
254
|
+
throw err;
|
|
255
|
+
} finally {
|
|
256
|
+
setLoading(false);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
updateItems: async (params: { orderItems: Array<{ index: number; quantity: number }> }) => {
|
|
261
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
262
|
+
if (!ofId) return;
|
|
263
|
+
setLoading(true);
|
|
264
|
+
try {
|
|
265
|
+
const updated = await invoke.vtex.actions.updateCartItems({
|
|
266
|
+
data: { orderFormId: ofId, orderItems: params.orderItems },
|
|
267
|
+
});
|
|
268
|
+
setOrderForm(updated);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error("[useCart] updateItems failed:", err);
|
|
271
|
+
} finally {
|
|
272
|
+
setLoading(false);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
removeItem: async (index: number) => {
|
|
277
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
278
|
+
if (!ofId) return;
|
|
279
|
+
setLoading(true);
|
|
280
|
+
try {
|
|
281
|
+
const updated = await invoke.vtex.actions.updateCartItems({
|
|
282
|
+
data: {
|
|
283
|
+
orderFormId: ofId,
|
|
284
|
+
orderItems: [{ index, quantity: 0 }],
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
setOrderForm(updated);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error("[useCart] removeItem failed:", err);
|
|
290
|
+
} finally {
|
|
291
|
+
setLoading(false);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
addCouponsToCart: async ({ text }: { text: string }) => {
|
|
296
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
297
|
+
if (!ofId) return;
|
|
298
|
+
setLoading(true);
|
|
299
|
+
try {
|
|
300
|
+
const updated = await invoke.vtex.actions.addCouponToCart({
|
|
301
|
+
data: { orderFormId: ofId, text },
|
|
302
|
+
});
|
|
303
|
+
setOrderForm(updated);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error("[useCart] addCoupon failed:", err);
|
|
306
|
+
} finally {
|
|
307
|
+
setLoading(false);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
sendAttachment: async (params: { attachment: string; body: Record<string, unknown> }) => {
|
|
312
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
313
|
+
if (!ofId) return;
|
|
314
|
+
setLoading(true);
|
|
315
|
+
try {
|
|
316
|
+
const updated = await invoke.vtex.actions.updateOrderFormAttachment({
|
|
317
|
+
data: {
|
|
318
|
+
orderFormId: ofId,
|
|
319
|
+
attachment: params.attachment,
|
|
320
|
+
body: params.body,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
setOrderForm(updated);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.error("[useCart] sendAttachment failed:", err);
|
|
326
|
+
} finally {
|
|
327
|
+
setLoading(false);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
simulate: async (data: {
|
|
332
|
+
items: Array<{ id: string; quantity: number; seller: string }>;
|
|
333
|
+
postalCode: string;
|
|
334
|
+
country: string;
|
|
335
|
+
}) => {
|
|
336
|
+
return await invoke.vtex.actions.simulateCart({
|
|
337
|
+
data: {
|
|
338
|
+
items: data.items.map((i) => ({
|
|
339
|
+
id: i.id,
|
|
340
|
+
quantity: i.quantity,
|
|
341
|
+
seller: i.seller,
|
|
342
|
+
})),
|
|
343
|
+
postalCode: data.postalCode,
|
|
344
|
+
country: data.country,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
mapItemsToAnalyticsItems: (orderForm: OrderForm | null) => {
|
|
350
|
+
return (orderForm?.items || []).map((item, index) => itemToAnalyticsItem(item, index));
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
useCart,
|
|
357
|
+
resetCart,
|
|
358
|
+
itemToAnalyticsItem,
|
|
359
|
+
};
|
|
360
|
+
}
|
package/vtex/hooks/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type CreateUseCartInvoke,
|
|
3
|
+
type CreateUseCartOptions,
|
|
4
|
+
createUseCart,
|
|
5
|
+
} from "./createUseCart";
|
|
1
6
|
export { type UseAutocompleteOptions, useAutocomplete } from "./useAutocomplete";
|
|
2
7
|
export { type CartItem, type OrderForm, type UseCartOptions, useCart } from "./useCart";
|
|
3
8
|
export { type UseUserOptions, useUser, type VtexUser } from "./useUser";
|