@decocms/apps-vtex 7.24.0 → 7.26.0
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 +12 -5
- package/src/actions/checkout.ts +475 -338
- package/src/hooks/__tests__/createCart.render.test.ts +114 -0
- package/src/hooks/__tests__/createCart.test.ts +92 -0
- package/src/hooks/cartQuery.ts +204 -0
- package/src/hooks/createCart.ts +427 -0
- package/src/hooks/index.ts +6 -0
- package/src/invoke.ts +159 -95
- package/src/loaders/cart/attachments.ts +53 -0
- package/src/loaders/cart/full.ts +67 -0
- package/src/loaders/cart/gifts.ts +43 -0
- package/src/loaders/cart/orderFormId.ts +31 -0
- package/src/loaders/cart/shipping.test.ts +38 -1
- package/src/loaders/cart/shipping.ts +67 -1
- package/src/loaders/cart/summary.ts +36 -0
- package/src/loaders/intelligentSearch/productDetailsPage.ts +20 -2
- package/src/loaders/paths/PDPDefaultPath.ts +51 -0
- package/src/manifest.gen.ts +48 -38
- package/src/utils/__tests__/cartProjection.test.ts +118 -0
- package/src/utils/cartProjection.ts +96 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Render-based tests for the `createCart` factory (Cart v2).
|
|
4
|
+
*
|
|
5
|
+
* The factory-shape tests in `createCart.test.ts` run in node and never render.
|
|
6
|
+
* These exercise the actual hook behaviour that needs a React renderer:
|
|
7
|
+
* `useAddToCart().add()` returning the server's projected payload (so a caller
|
|
8
|
+
* can drive a toast without a second fetch) and the optimistic badge bump.
|
|
9
|
+
*
|
|
10
|
+
* No `@testing-library/react` in the tree — we hand-roll a tiny render harness
|
|
11
|
+
* on `react-dom/client` (jsdom env, declared in the docblock above).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { CartSummaryWithItems } from "@decocms/apps-commerce/types";
|
|
15
|
+
import { act, createElement } from "react";
|
|
16
|
+
import { createRoot } from "react-dom/client";
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { type CreateCartInvoke, createCart } from "../createCart";
|
|
19
|
+
|
|
20
|
+
// React's act() requires this flag when driving updates manually.
|
|
21
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
+
|
|
23
|
+
function renderHook<T>(useHook: () => T) {
|
|
24
|
+
const ref = { current: undefined as unknown as T };
|
|
25
|
+
function Probe() {
|
|
26
|
+
ref.current = useHook();
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const container = document.createElement("div");
|
|
30
|
+
const root = createRoot(container);
|
|
31
|
+
act(() => {
|
|
32
|
+
root.render(createElement(Probe));
|
|
33
|
+
});
|
|
34
|
+
return { ref, unmount: () => act(() => root.unmount()) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Fake invoke whose addItemsToCartV2 returns a `summary+items` projection. */
|
|
38
|
+
function makeInvoke(): CreateCartInvoke {
|
|
39
|
+
const projected: CartSummaryWithItems = {
|
|
40
|
+
orderFormId: "of-1",
|
|
41
|
+
totalItems: 1,
|
|
42
|
+
total: 100,
|
|
43
|
+
items: [
|
|
44
|
+
{
|
|
45
|
+
item_id: "sku-1",
|
|
46
|
+
item_name: "Camiseta",
|
|
47
|
+
item_variant: "P Azul",
|
|
48
|
+
image: "https://img.example/x.jpg",
|
|
49
|
+
price: 100,
|
|
50
|
+
quantity: 1,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
vtex: {
|
|
56
|
+
actions: {
|
|
57
|
+
getOrCreateCartV2: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
58
|
+
addItemsToCartV2: async () => projected as never,
|
|
59
|
+
updateCartItemsV2: async () => ({ ok: true }) as never,
|
|
60
|
+
addCouponToCartV2: async () => ({ ok: true }) as never,
|
|
61
|
+
},
|
|
62
|
+
loaders: {
|
|
63
|
+
cart: {
|
|
64
|
+
summary: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
65
|
+
full: async () => ({ original: null }) as never,
|
|
66
|
+
shipping: async () => ({ postalCode: "00000-000", options: [] }),
|
|
67
|
+
gifts: async () => ({ orderFormId: "of-1", selectableGifts: [], ratesAndBenefits: null }),
|
|
68
|
+
attachments: async () => ({
|
|
69
|
+
orderFormId: "of-1",
|
|
70
|
+
itemIndex: 0,
|
|
71
|
+
attachments: [],
|
|
72
|
+
attachmentOfferings: [],
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("createCart — useAddToCart.add()", () => {
|
|
81
|
+
it("returns the projected payload for a toast without a second fetch", async () => {
|
|
82
|
+
const cart = createCart({ invoke: makeInvoke() });
|
|
83
|
+
const { ref, unmount } = renderHook(() => cart.useAddToCart());
|
|
84
|
+
|
|
85
|
+
let result: unknown;
|
|
86
|
+
await act(async () => {
|
|
87
|
+
result = await ref.current.add({ id: "sku-1", seller: "1" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result).toMatchObject({
|
|
91
|
+
totalItems: 1,
|
|
92
|
+
total: 100,
|
|
93
|
+
items: [{ item_name: "Camiseta", item_variant: "P Azul", price: 100 }],
|
|
94
|
+
});
|
|
95
|
+
unmount();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reconciles the summary badge from the projected response", async () => {
|
|
99
|
+
const cart = createCart({ invoke: makeInvoke() });
|
|
100
|
+
const add = renderHook(() => cart.useAddToCart());
|
|
101
|
+
const badge = renderHook(() => cart.useCartSummary());
|
|
102
|
+
|
|
103
|
+
expect(badge.ref.current.totalItems).toBe(0);
|
|
104
|
+
|
|
105
|
+
await act(async () => {
|
|
106
|
+
await add.ref.current.add({ id: "sku-1", seller: "1" });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Reconciled from the server projection (1), not just the optimistic bump.
|
|
110
|
+
expect(badge.ref.current.totalItems).toBe(1);
|
|
111
|
+
add.unmount();
|
|
112
|
+
badge.unmount();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `createCart` factory (Cart v2).
|
|
3
|
+
*
|
|
4
|
+
* Like `createUseCart`, hook render semantics need a React renderer that
|
|
5
|
+
* apps-start does not pull in, so we cover the factory's shape and instance
|
|
6
|
+
* isolation. The optimistic + reconciliation behaviour is exercised by the
|
|
7
|
+
* site-level smoke test.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { type CreateCartInvoke, createCart } from "../createCart";
|
|
12
|
+
|
|
13
|
+
function makeInvoke(): CreateCartInvoke {
|
|
14
|
+
const summary = async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 });
|
|
15
|
+
const anyNoop = async () => ({ ok: true }) as never;
|
|
16
|
+
return {
|
|
17
|
+
vtex: {
|
|
18
|
+
actions: {
|
|
19
|
+
getOrCreateCartV2: async () => ({ orderFormId: "of-1", totalItems: 0, total: 0 }),
|
|
20
|
+
addItemsToCartV2: anyNoop,
|
|
21
|
+
updateCartItemsV2: anyNoop,
|
|
22
|
+
addCouponToCartV2: anyNoop,
|
|
23
|
+
},
|
|
24
|
+
loaders: {
|
|
25
|
+
cart: {
|
|
26
|
+
summary,
|
|
27
|
+
full: async () => ({
|
|
28
|
+
original: null,
|
|
29
|
+
storefront: {
|
|
30
|
+
items: [],
|
|
31
|
+
total: 0,
|
|
32
|
+
subtotal: 0,
|
|
33
|
+
discounts: 0,
|
|
34
|
+
locale: "pt-BR",
|
|
35
|
+
currency: "BRL",
|
|
36
|
+
freeShippingTarget: 0,
|
|
37
|
+
checkoutHref: "/checkout",
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
shipping: async () => ({ postalCode: "00000-000", options: [] }),
|
|
41
|
+
gifts: async () => ({ orderFormId: "of-1", selectableGifts: [], ratesAndBenefits: null }),
|
|
42
|
+
attachments: async () => ({
|
|
43
|
+
orderFormId: "of-1",
|
|
44
|
+
itemIndex: 0,
|
|
45
|
+
attachments: [],
|
|
46
|
+
attachmentOfferings: [],
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("createCart — factory shape", () => {
|
|
55
|
+
it("returns the full granular hook set", () => {
|
|
56
|
+
const cart = createCart({ invoke: makeInvoke() });
|
|
57
|
+
expect(typeof cart.useCart).toBe("function");
|
|
58
|
+
expect(typeof cart.useCartSummary).toBe("function");
|
|
59
|
+
expect(typeof cart.useAddToCart).toBe("function");
|
|
60
|
+
expect(typeof cart.useShipping).toBe("function");
|
|
61
|
+
expect(typeof cart.useGifts).toBe("function");
|
|
62
|
+
expect(typeof cart.useAttachments).toBe("function");
|
|
63
|
+
expect(typeof cart.resetCart).toBe("function");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("two factory calls produce independent hook identities", () => {
|
|
67
|
+
const a = createCart({ invoke: makeInvoke() });
|
|
68
|
+
const b = createCart({ invoke: makeInvoke() });
|
|
69
|
+
expect(a.useCart).not.toBe(b.useCart);
|
|
70
|
+
expect(a.useAddToCart).not.toBe(b.useAddToCart);
|
|
71
|
+
expect(a.resetCart).not.toBe(b.resetCart);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("accepts custom cookie name / max-age without throwing", () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
createCart({
|
|
77
|
+
invoke: makeInvoke(),
|
|
78
|
+
orderFormCookieName: "custom_of",
|
|
79
|
+
orderFormCookieMaxAge: 3600,
|
|
80
|
+
}),
|
|
81
|
+
).not.toThrow();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("does not call any invoke method at construction time (lazy)", () => {
|
|
85
|
+
const invoke = makeInvoke();
|
|
86
|
+
const getOrCreate = vi.spyOn(invoke.vtex.actions, "getOrCreateCartV2");
|
|
87
|
+
const summary = vi.spyOn(invoke.vtex.loaders.cart, "summary");
|
|
88
|
+
createCart({ invoke });
|
|
89
|
+
expect(getOrCreate).not.toHaveBeenCalled();
|
|
90
|
+
expect(summary).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart v2 — optional TanStack Query adapter.
|
|
3
|
+
*
|
|
4
|
+
* The base factory (`createCart`) is intentionally QueryClient-free so it runs
|
|
5
|
+
* anywhere. Sites already invested in TanStack Query can use this adapter
|
|
6
|
+
* instead to get cache integration, `staleTime`, devtools, and cross-component
|
|
7
|
+
* query sharing — while keeping the exact same invoke injection and the same
|
|
8
|
+
* server-side fragmentation/projection contract.
|
|
9
|
+
*
|
|
10
|
+
* Importing this module pulls in `@tanstack/react-query` (a peer dependency).
|
|
11
|
+
* If your site does not use react-query, import `createCart` instead.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createCartQuery } from "@decocms/apps/vtex/hooks/cartQuery";
|
|
16
|
+
* import { invoke } from "~/server/invoke";
|
|
17
|
+
* export const { useCartSummary, useCartFull, useAddToCart } = createCartQuery({ invoke });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
CartProjection,
|
|
23
|
+
CartSection,
|
|
24
|
+
CartSummary,
|
|
25
|
+
Minicart,
|
|
26
|
+
} from "@decocms/apps-commerce/types";
|
|
27
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
28
|
+
import type { CartItemAttachments } from "../loaders/cart/attachments";
|
|
29
|
+
import type { CartGifts } from "../loaders/cart/gifts";
|
|
30
|
+
import type { CartShipping } from "../loaders/cart/shipping";
|
|
31
|
+
import type { OrderForm } from "../types";
|
|
32
|
+
import type { CreateCartInvoke } from "./createCart";
|
|
33
|
+
|
|
34
|
+
export interface CreateCartQueryOptions {
|
|
35
|
+
invoke: CreateCartInvoke;
|
|
36
|
+
orderFormCookieName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SUMMARY_KEY = ["vtex", "cart", "summary"] as const;
|
|
40
|
+
const FULL_KEY = ["vtex", "cart", "full"] as const;
|
|
41
|
+
const SHIPPING_KEY = ["vtex", "cart", "shipping"] as const;
|
|
42
|
+
const GIFTS_KEY = ["vtex", "cart", "gifts"] as const;
|
|
43
|
+
const ATTACHMENTS_KEY = ["vtex", "cart", "attachments"] as const;
|
|
44
|
+
|
|
45
|
+
/** TanStack Query flavour of the Cart v2 hooks. */
|
|
46
|
+
export function createCartQuery(opts: CreateCartQueryOptions) {
|
|
47
|
+
const { invoke } = opts;
|
|
48
|
+
const COOKIE_NAME = opts.orderFormCookieName ?? "checkout.vtex.com__orderFormId";
|
|
49
|
+
|
|
50
|
+
function escapeRegex(s: string): string {
|
|
51
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
function getOrderFormIdFromCookie(): string | null {
|
|
54
|
+
if (typeof document === "undefined") return null;
|
|
55
|
+
const m = document.cookie.match(new RegExp(`${escapeRegex(COOKIE_NAME)}=([^;]*)`));
|
|
56
|
+
return m ? decodeURIComponent(m[1]) : null;
|
|
57
|
+
}
|
|
58
|
+
function setOrderFormIdCookie(id: string) {
|
|
59
|
+
if (typeof document === "undefined") return;
|
|
60
|
+
// biome-ignore lint/suspicious/noDocumentCookie: the orderFormId cookie is intentionally client-readable (VTEX standard).
|
|
61
|
+
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(id)}; path=/; max-age=${7 * 24 * 3600}; SameSite=Lax`;
|
|
62
|
+
}
|
|
63
|
+
async function ensureOrderFormId(current: string | null): Promise<string> {
|
|
64
|
+
const existing = current ?? getOrderFormIdFromCookie();
|
|
65
|
+
if (existing) return existing;
|
|
66
|
+
const created = (await invoke.vtex.actions.getOrCreateCartV2({
|
|
67
|
+
data: { projection: "summary" },
|
|
68
|
+
})) as CartSummary;
|
|
69
|
+
if (created.orderFormId) setOrderFormIdCookie(created.orderFormId);
|
|
70
|
+
return created.orderFormId ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Badge query. Lazy: `enabled` defaults to false so it only runs when the
|
|
75
|
+
* caller opts in (e.g. after the cart cookie exists) — no cart is created on
|
|
76
|
+
* mount.
|
|
77
|
+
*/
|
|
78
|
+
function useCartSummary(options?: { enabled?: boolean; staleTime?: number }) {
|
|
79
|
+
return useQuery({
|
|
80
|
+
queryKey: SUMMARY_KEY,
|
|
81
|
+
queryFn: () => invoke.vtex.loaders.cart.summary({ data: {} }),
|
|
82
|
+
enabled: options?.enabled ?? false,
|
|
83
|
+
staleTime: options?.staleTime ?? 30_000,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Full drawer query. `enabled` gated so it fires when the drawer opens. */
|
|
88
|
+
function useCartFull(options?: {
|
|
89
|
+
enabled?: boolean;
|
|
90
|
+
staleTime?: number;
|
|
91
|
+
freeShippingTarget?: number;
|
|
92
|
+
locale?: string;
|
|
93
|
+
checkoutHref?: string;
|
|
94
|
+
enableCoupon?: boolean;
|
|
95
|
+
}) {
|
|
96
|
+
return useQuery<Minicart<OrderForm | null>>({
|
|
97
|
+
queryKey: FULL_KEY,
|
|
98
|
+
queryFn: () =>
|
|
99
|
+
invoke.vtex.loaders.cart.full({
|
|
100
|
+
data: {
|
|
101
|
+
freeShippingTarget: options?.freeShippingTarget,
|
|
102
|
+
locale: options?.locale,
|
|
103
|
+
checkoutHref: options?.checkoutHref,
|
|
104
|
+
enableCoupon: options?.enableCoupon,
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
enabled: options?.enabled ?? false,
|
|
108
|
+
staleTime: options?.staleTime ?? 30_000,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function useAddToCart(hookOpts?: { projection?: CartProjection; sections?: CartSection[] }) {
|
|
113
|
+
const queryClient = useQueryClient();
|
|
114
|
+
const projection: CartProjection = hookOpts?.projection ?? "summary+items";
|
|
115
|
+
|
|
116
|
+
return useMutation({
|
|
117
|
+
mutationFn: async (params: { id: string; seller: string; quantity?: number }) => {
|
|
118
|
+
const current = queryClient.getQueryData<CartSummary>(SUMMARY_KEY)?.orderFormId ?? null;
|
|
119
|
+
const orderFormId = await ensureOrderFormId(current);
|
|
120
|
+
return invoke.vtex.actions.addItemsToCartV2({
|
|
121
|
+
data: {
|
|
122
|
+
orderFormId,
|
|
123
|
+
orderItems: [{ id: params.id, seller: params.seller, quantity: params.quantity ?? 1 }],
|
|
124
|
+
projection,
|
|
125
|
+
sections: hookOpts?.sections,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
// Optimistic badge bump.
|
|
130
|
+
onMutate: async (params) => {
|
|
131
|
+
await queryClient.cancelQueries({ queryKey: SUMMARY_KEY });
|
|
132
|
+
const prev = queryClient.getQueryData<CartSummary>(SUMMARY_KEY);
|
|
133
|
+
const qty = params.quantity ?? 1;
|
|
134
|
+
queryClient.setQueryData<CartSummary>(SUMMARY_KEY, (s) => ({
|
|
135
|
+
orderFormId: s?.orderFormId ?? null,
|
|
136
|
+
total: s?.total ?? 0,
|
|
137
|
+
totalItems: (s?.totalItems ?? 0) + qty,
|
|
138
|
+
}));
|
|
139
|
+
return { prev };
|
|
140
|
+
},
|
|
141
|
+
onError: (_err, _params, ctx) => {
|
|
142
|
+
if (ctx?.prev) queryClient.setQueryData(SUMMARY_KEY, ctx.prev);
|
|
143
|
+
},
|
|
144
|
+
onSuccess: (result) => {
|
|
145
|
+
if (projection === "minicart") {
|
|
146
|
+
queryClient.setQueryData(FULL_KEY, result);
|
|
147
|
+
} else if (projection !== "none") {
|
|
148
|
+
const r = result as Partial<CartSummary>;
|
|
149
|
+
if (r && typeof r.totalItems === "number") {
|
|
150
|
+
queryClient.setQueryData<CartSummary>(SUMMARY_KEY, {
|
|
151
|
+
orderFormId: r.orderFormId ?? null,
|
|
152
|
+
totalItems: r.totalItems,
|
|
153
|
+
total: r.total ?? 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Shipping estimate query. Keyed by `{ postalCode, items }` — which is NOT
|
|
163
|
+
* user-personalized — so react-query's cache/dedupe gives you the caching
|
|
164
|
+
* the server-side loader can't do yet (see the loader's caching note).
|
|
165
|
+
* `enabled` defaults on when a postal code is present.
|
|
166
|
+
*/
|
|
167
|
+
function useShipping(
|
|
168
|
+
params: {
|
|
169
|
+
items: Array<{ id: string | number; quantity: number; seller: string }>;
|
|
170
|
+
postalCode: string;
|
|
171
|
+
country?: string;
|
|
172
|
+
},
|
|
173
|
+
options?: { enabled?: boolean; staleTime?: number },
|
|
174
|
+
) {
|
|
175
|
+
return useQuery<CartShipping>({
|
|
176
|
+
queryKey: [...SHIPPING_KEY, params.postalCode, params.items],
|
|
177
|
+
queryFn: () => invoke.vtex.loaders.cart.shipping({ data: params }),
|
|
178
|
+
enabled: options?.enabled ?? Boolean(params.postalCode && params.items.length),
|
|
179
|
+
staleTime: options?.staleTime ?? 5 * 60_000,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Selectable-gifts / promotions query. `enabled` gated (default false). */
|
|
184
|
+
function useGifts(options?: { enabled?: boolean; staleTime?: number }) {
|
|
185
|
+
return useQuery<CartGifts>({
|
|
186
|
+
queryKey: GIFTS_KEY,
|
|
187
|
+
queryFn: () => invoke.vtex.loaders.cart.gifts({ data: {} }),
|
|
188
|
+
enabled: options?.enabled ?? false,
|
|
189
|
+
staleTime: options?.staleTime ?? 30_000,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Single-line attachments query. `enabled` gated (default false). */
|
|
194
|
+
function useAttachments(itemIndex: number, options?: { enabled?: boolean; staleTime?: number }) {
|
|
195
|
+
return useQuery<CartItemAttachments>({
|
|
196
|
+
queryKey: [...ATTACHMENTS_KEY, itemIndex],
|
|
197
|
+
queryFn: () => invoke.vtex.loaders.cart.attachments({ data: { itemIndex } }),
|
|
198
|
+
enabled: options?.enabled ?? false,
|
|
199
|
+
staleTime: options?.staleTime ?? 30_000,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { useCartSummary, useCartFull, useAddToCart, useShipping, useGifts, useAttachments };
|
|
204
|
+
}
|