@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.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.
Files changed (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. package/tsconfig.json +0 -7
@@ -1,242 +0,0 @@
1
- /**
2
- * Factory for the legacy invoke-based `useWishlist` hook.
3
- *
4
- * Mirrors the deco-cx/apps signal-based wishlist API used by migrated
5
- * Fresh sites: `wishlist.addItem(productId, productGroupId)`,
6
- * `removeItem(productId)`, `getItem(productId): boolean`.
7
- *
8
- * It is intentionally separate from the canonical `useWishlist` in
9
- * `vtex/hooks/useWishlist.ts`, which is built on TanStack Query and exposes
10
- * `{ items, isInWishlist, toggle, add, remove }`. Both can coexist.
11
- *
12
- * ## VTEX wishlist arg conventions
13
- *
14
- * The legacy hook's `addItem(productId, productGroupId)` argument names
15
- * are misleading because they were originally derived from analytics
16
- * `item_id` / `item_group_id`:
17
- *
18
- * - `productId` arg → analytics `item_id` → VTEX `sku` field on the wishlist
19
- * - `productGroupId` arg → analytics `item_group_id` → VTEX `productId`
20
- *
21
- * The factory swaps them on the wire so the canonical
22
- * `vtex/actions/wishlist.addItem` gets the right shape.
23
- *
24
- * @example
25
- * ```ts
26
- * // src/hooks/useWishlist.ts
27
- * import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist";
28
- * import { invoke } from "~/server/invoke";
29
- *
30
- * export const { useWishlist, resetWishlist } = createUseWishlist({ invoke });
31
- * export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist";
32
- * ```
33
- */
34
-
35
- import { useEffect, useState } from "react";
36
- import type { WishlistItem } from "../loaders/wishlist";
37
-
38
- /**
39
- * Pure helper: find a wishlist entry by either the SKU id (legacy
40
- * `productId` arg) or the VTEX productId. Exported for unit testability.
41
- */
42
- export function findWishlistEntry(
43
- items: readonly WishlistItem[],
44
- productId: string,
45
- ): WishlistItem | undefined {
46
- return items.find((it) => it.sku === productId || it.productId === productId);
47
- }
48
-
49
- /**
50
- * Pure helper: convert legacy `addItem(productId, productGroupId)` args
51
- * into the canonical `{ productId, sku }` shape expected by
52
- * `vtex/actions/wishlist.addItem`. Exported for unit testability.
53
- */
54
- export function legacyAddArgsToCanonical(
55
- legacyProductId: string,
56
- legacyProductGroupId: string,
57
- ): { productId: string; sku: string } {
58
- // See arg conventions in the file header. The legacy `productId` is
59
- // the SKU; the legacy `productGroupId` is the VTEX productId.
60
- return {
61
- productId: legacyProductGroupId,
62
- sku: legacyProductId,
63
- };
64
- }
65
-
66
- /** Minimal structural shape of the invoke proxy this hook needs. */
67
- export interface CreateUseWishlistInvoke {
68
- vtex: {
69
- loaders: {
70
- wishlist: () => Promise<WishlistItem[]>;
71
- };
72
- actions: {
73
- addToWishlist: (args: {
74
- data: { productId: string; sku: string; title?: string };
75
- }) => Promise<WishlistItem[]>;
76
- removeFromWishlist: (args: { data: { id: string } }) => Promise<WishlistItem[]>;
77
- };
78
- };
79
- }
80
-
81
- export interface CreateUseWishlistOptions {
82
- invoke: CreateUseWishlistInvoke;
83
- }
84
-
85
- /** Build a per-site `useWishlist` plus its companions. */
86
- export function createUseWishlist(opts: CreateUseWishlistOptions) {
87
- const { invoke } = opts;
88
-
89
- let _items: WishlistItem[] = [];
90
- let _loading = false;
91
- let _initStarted = false;
92
- let _initFailed = false;
93
- const _listeners = new Set<() => void>();
94
-
95
- function notify() {
96
- for (const fn of _listeners) fn();
97
- }
98
- function setItems(items: WishlistItem[]) {
99
- _items = items;
100
- notify();
101
- }
102
- function setLoading(v: boolean) {
103
- _loading = v;
104
- notify();
105
- }
106
-
107
- function getItem(productId: string): boolean {
108
- return !!findWishlistEntry(_items, productId);
109
- }
110
-
111
- async function addItem(productId: string, productGroupId: string): Promise<void> {
112
- setLoading(true);
113
- try {
114
- const updated = await invoke.vtex.actions.addToWishlist({
115
- data: legacyAddArgsToCanonical(productId, productGroupId),
116
- });
117
- setItems(updated);
118
- } catch (err) {
119
- console.error("[useWishlist] addItem failed:", err);
120
- throw err;
121
- } finally {
122
- setLoading(false);
123
- }
124
- }
125
-
126
- async function removeItem(productId: string): Promise<void> {
127
- const entry = findWishlistEntry(_items, productId);
128
- if (!entry?.id) {
129
- // Either the wishlist hasn't loaded yet or the item isn't there.
130
- // Either way, nothing to remove.
131
- return;
132
- }
133
- setLoading(true);
134
- try {
135
- const updated = await invoke.vtex.actions.removeFromWishlist({
136
- data: { id: entry.id },
137
- });
138
- setItems(updated);
139
- } catch (err) {
140
- console.error("[useWishlist] removeItem failed:", err);
141
- throw err;
142
- } finally {
143
- setLoading(false);
144
- }
145
- }
146
-
147
- async function refresh(): Promise<WishlistItem[]> {
148
- setLoading(true);
149
- try {
150
- const items = await invoke.vtex.loaders.wishlist();
151
- setItems(items);
152
- _initFailed = false;
153
- return items;
154
- } catch (err) {
155
- console.error("[useWishlist] refresh failed:", err);
156
- _initFailed = true;
157
- notify();
158
- return [];
159
- } finally {
160
- setLoading(false);
161
- }
162
- }
163
-
164
- /** Reset module-level wishlist state so the next useWishlist() re-fetches. */
165
- function resetWishlist() {
166
- _items = [];
167
- _loading = false;
168
- _initStarted = false;
169
- _initFailed = false;
170
- notify();
171
- }
172
-
173
- function useWishlist() {
174
- const [, forceRender] = useState(0);
175
-
176
- useEffect(() => {
177
- const listener = () => forceRender((n) => n + 1);
178
- _listeners.add(listener);
179
-
180
- if (_items.length === 0 && !_initStarted) {
181
- _initStarted = true;
182
- setLoading(true);
183
- invoke.vtex.loaders
184
- .wishlist()
185
- .then((items) => {
186
- setItems(items);
187
- })
188
- .catch((err: unknown) => {
189
- // 401 / unauthenticated is normal — user just isn't logged in.
190
- // Real errors get logged.
191
- console.error("[useWishlist] init failed:", err);
192
- _initFailed = true;
193
- notify();
194
- })
195
- .finally(() => setLoading(false));
196
- }
197
-
198
- return () => {
199
- _listeners.delete(listener);
200
- };
201
- }, []);
202
-
203
- return {
204
- items: {
205
- get value() {
206
- return _items;
207
- },
208
- set value(v: WishlistItem[]) {
209
- setItems(v);
210
- },
211
- },
212
-
213
- loading: {
214
- get value() {
215
- return _loading;
216
- },
217
- },
218
-
219
- initFailed: {
220
- get value() {
221
- return _initFailed;
222
- },
223
- },
224
-
225
- count: {
226
- get value() {
227
- return _items.length;
228
- },
229
- },
230
-
231
- addItem,
232
- removeItem,
233
- getItem,
234
- refresh,
235
- };
236
- }
237
-
238
- return {
239
- useWishlist,
240
- resetWishlist,
241
- };
242
- }
@@ -1,25 +0,0 @@
1
- export {
2
- type CreateCartInvoke,
3
- type CreateCartOptions,
4
- createCart,
5
- } from "./createCart";
6
- export { type CreateCartQueryOptions, createCartQuery } from "./cartQuery";
7
- export {
8
- type CreateUseCartInvoke,
9
- type CreateUseCartOptions,
10
- createUseCart,
11
- } from "./createUseCart";
12
- export {
13
- type CreateUseUserInvoke,
14
- type CreateUseUserOptions,
15
- createUseUser,
16
- } from "./createUseUser";
17
- export {
18
- type CreateUseWishlistInvoke,
19
- type CreateUseWishlistOptions,
20
- createUseWishlist,
21
- } from "./createUseWishlist";
22
- export { type UseAutocompleteOptions, useAutocomplete } from "./useAutocomplete";
23
- export { type CartItem, type OrderForm, type UseCartOptions, useCart } from "./useCart";
24
- export { type UseUserOptions, useUser, type VtexUser } from "./useUser";
25
- export { type UseWishlistOptions, useWishlist, type WishlistItem } from "./useWishlist";
@@ -1,83 +0,0 @@
1
- /**
2
- * Client-side autocomplete hook for VTEX Intelligent Search.
3
- *
4
- * Uses TanStack Query with debounced input to fetch search suggestions.
5
- * Ported from deco-cx/apps vtex/hooks/useAutocomplete.ts
6
- *
7
- * @example
8
- * ```tsx
9
- * import { useAutocomplete } from "@decocms/apps/vtex/hooks/useAutocomplete";
10
- *
11
- * function SearchBar() {
12
- * const { setSearch, suggestions, loading } = useAutocomplete();
13
- * return (
14
- * <input
15
- * onChange={(e) => setSearch(e.target.value)}
16
- * placeholder="Search..."
17
- * />
18
- * );
19
- * }
20
- * ```
21
- */
22
-
23
- import { useQuery } from "@tanstack/react-query";
24
- import { useCallback, useEffect, useState } from "react";
25
- import type { Suggestion } from "@decocms/apps-commerce/types";
26
-
27
- export interface UseAutocompleteOptions {
28
- /** Debounce delay in ms @default 250 */
29
- debounceMs?: number;
30
- /** Max products to return @default 4 */
31
- count?: number;
32
- /** Custom fetch function — defaults to calling the inline-loader on the server */
33
- fetchSuggestions?: (query: string, count: number) => Promise<Suggestion | null>;
34
- }
35
-
36
- const AUTOCOMPLETE_QUERY_KEY = "vtex-autocomplete";
37
-
38
- function useDebounce<T>(value: T, delay: number): T {
39
- const [debounced, setDebounced] = useState(value);
40
- useEffect(() => {
41
- const timer = setTimeout(() => setDebounced(value), delay);
42
- return () => clearTimeout(timer);
43
- }, [value, delay]);
44
- return debounced;
45
- }
46
-
47
- async function defaultFetchSuggestions(query: string, _count: number): Promise<Suggestion | null> {
48
- const params = new URLSearchParams({ query });
49
- const res = await fetch(`/api/vtex/suggestions?${params}`);
50
- if (!res.ok) return null;
51
- return res.json();
52
- }
53
-
54
- export function useAutocomplete(opts?: UseAutocompleteOptions) {
55
- const debounceMs = opts?.debounceMs ?? 250;
56
- const count = opts?.count ?? 4;
57
- const fetchFn = opts?.fetchSuggestions ?? defaultFetchSuggestions;
58
-
59
- const [rawQuery, setRawQuery] = useState("");
60
- const debouncedQuery = useDebounce(rawQuery.trim(), debounceMs);
61
-
62
- const { data, isLoading, isFetching } = useQuery({
63
- queryKey: [AUTOCOMPLETE_QUERY_KEY, debouncedQuery, count],
64
- queryFn: () => fetchFn(debouncedQuery, count),
65
- enabled: debouncedQuery.length > 0,
66
- staleTime: 60_000,
67
- });
68
-
69
- const setSearch = useCallback((query: string) => {
70
- setRawQuery(query);
71
- }, []);
72
-
73
- return {
74
- /** Set the search query (will be debounced automatically) */
75
- setSearch,
76
- /** Current raw (un-debounced) query */
77
- query: rawQuery,
78
- /** Suggestion result (searches + products) */
79
- suggestions: data ?? null,
80
- /** True while initial fetch is in progress */
81
- loading: isLoading || isFetching,
82
- };
83
- }
@@ -1,243 +0,0 @@
1
- /**
2
- * Client-side cart hook for VTEX.
3
- *
4
- * Uses TanStack Query for SWR, optimistic updates, and cache invalidation.
5
- * Returns BOTH the raw `OrderForm` (back-compat for existing consumers) AND
6
- * the canonical `Minicart` shape (preferred for new code).
7
- *
8
- * @example Reading the cart
9
- * ```tsx
10
- * import { useCart } from "@decocms/apps/vtex/hooks/useCart";
11
- *
12
- * function CartButton() {
13
- * const { minicart, isLoading } = useCart({ freeShippingTarget: 200 });
14
- * const count = minicart?.storefront.items.length ?? 0;
15
- * return <button disabled={isLoading}>{count} items</button>;
16
- * }
17
- * ```
18
- *
19
- * @example Mutations
20
- * ```tsx
21
- * const { addItems, removeItem, addCoupons } = useCart();
22
- * addItems.mutate([{ id: "123", seller: "1", quantity: 1 }]);
23
- * ```
24
- */
25
-
26
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
27
- import { useMemo } from "react";
28
- import type { Minicart } from "@decocms/apps-commerce/types";
29
- import type { OrderForm, OrderFormItem } from "../types";
30
- import { vtexOrderFormToMinicart } from "../utils/minicart";
31
-
32
- /** Re-exported from `vtex/types` for back-compat. New code should import directly. */
33
- export type { OrderForm } from "../types";
34
-
35
- /**
36
- * Slim cart-item shape used by mutations.
37
- * @deprecated Use `OrderFormItem` from `@decocms/apps/vtex/types` for full fidelity,
38
- * or `MinicartItem` from `@decocms/apps/commerce/types` for the canonical contract.
39
- */
40
- export type CartItem = Pick<OrderFormItem, "id" | "quantity" | "seller">;
41
-
42
- const CART_QUERY_KEY = ["vtex", "cart"] as const;
43
-
44
- const DEFAULT_EXPECTED_SECTIONS = [
45
- "items",
46
- "totalizers",
47
- "clientProfileData",
48
- "shippingData",
49
- "paymentData",
50
- "sellers",
51
- "messages",
52
- "marketingData",
53
- "clientPreferencesData",
54
- "storePreferencesData",
55
- "giftRegistryData",
56
- "ratesAndBenefitsData",
57
- "openTextField",
58
- "commercialConditionData",
59
- "customData",
60
- ];
61
-
62
- function getScParam(): string {
63
- if (typeof window !== "undefined") {
64
- const match = document.cookie.match(/(?:^|;\s*)VTEXSC=([^;]+)/);
65
- return match?.[1] ?? "";
66
- }
67
- return "";
68
- }
69
-
70
- function appendSc(url: string): string {
71
- const sc = getScParam();
72
- if (!sc) return url;
73
- return url.includes("?") ? `${url}&sc=${sc}` : `${url}?sc=${sc}`;
74
- }
75
-
76
- async function fetchCart(): Promise<OrderForm> {
77
- const res = await fetch(appendSc("/api/checkout/pub/orderForm"), {
78
- method: "POST",
79
- headers: { "Content-Type": "application/json" },
80
- body: JSON.stringify({ expectedOrderFormSections: DEFAULT_EXPECTED_SECTIONS }),
81
- credentials: "include",
82
- });
83
- if (!res.ok) throw new Error(`Cart fetch failed: ${res.status}`);
84
- return res.json();
85
- }
86
-
87
- async function addItemsToCart(
88
- orderFormId: string,
89
- items: Array<{ id: string; quantity: number; seller: string }>,
90
- ): Promise<OrderForm> {
91
- const params = new URLSearchParams();
92
- params.append("allowedOutdatedData", "paymentData");
93
- const sc = getScParam();
94
- if (sc) params.set("sc", sc);
95
-
96
- const res = await fetch(`/api/checkout/pub/orderForm/${orderFormId}/items?${params}`, {
97
- method: "POST",
98
- headers: { "Content-Type": "application/json" },
99
- body: JSON.stringify({ orderItems: items }),
100
- credentials: "include",
101
- });
102
- if (!res.ok) throw new Error(`Add to cart failed: ${res.status}`);
103
- return res.json();
104
- }
105
-
106
- async function addCouponsToCart(orderFormId: string, text: string): Promise<OrderForm> {
107
- const params = new URLSearchParams();
108
- const sc = getScParam();
109
- if (sc) params.set("sc", sc);
110
-
111
- const res = await fetch(`/api/checkout/pub/orderForm/${orderFormId}/coupons?${params}`, {
112
- method: "POST",
113
- headers: { "Content-Type": "application/json" },
114
- body: JSON.stringify({ text }),
115
- credentials: "include",
116
- });
117
- if (!res.ok) throw new Error(`Add coupon failed: ${res.status}`);
118
- return res.json();
119
- }
120
-
121
- async function updateItemQuantity(
122
- orderFormId: string,
123
- index: number,
124
- quantity: number,
125
- ): Promise<OrderForm> {
126
- const params = new URLSearchParams();
127
- params.append("allowedOutdatedData", "paymentData");
128
- const sc = getScParam();
129
- if (sc) params.set("sc", sc);
130
-
131
- const res = await fetch(`/api/checkout/pub/orderForm/${orderFormId}/items/update?${params}`, {
132
- method: "POST",
133
- headers: { "Content-Type": "application/json" },
134
- body: JSON.stringify({ orderItems: [{ index, quantity }] }),
135
- credentials: "include",
136
- });
137
- if (!res.ok) throw new Error(`Update quantity failed: ${res.status}`);
138
- return res.json();
139
- }
140
-
141
- export interface UseCartOptions {
142
- /** Enable automatic refetching. @default true */
143
- enabled?: boolean;
144
- /** Stale time in ms. @default 30000 */
145
- staleTime?: number;
146
- /** Free-shipping threshold in major units, surfaced on `minicart.storefront`. @default 0 */
147
- freeShippingTarget?: number;
148
- /** Override the OrderForm's locale (BCP-47, e.g. `"pt-BR"`). */
149
- locale?: string;
150
- /** Where the checkout button sends the user. @default "/checkout" */
151
- checkoutHref?: string;
152
- /** Whether to surface the coupon input. @default true */
153
- enableCoupon?: boolean;
154
- }
155
-
156
- export function useCart(options?: UseCartOptions) {
157
- const queryClient = useQueryClient();
158
-
159
- const query = useQuery({
160
- queryKey: CART_QUERY_KEY,
161
- queryFn: fetchCart,
162
- staleTime: options?.staleTime ?? 30_000,
163
- enabled: options?.enabled !== false,
164
- });
165
-
166
- const cart = query.data ?? null;
167
-
168
- const minicart: Minicart<OrderForm> | null = useMemo(() => {
169
- if (!cart) return null;
170
- return vtexOrderFormToMinicart(cart, {
171
- freeShippingTarget: options?.freeShippingTarget,
172
- locale: options?.locale,
173
- checkoutHref: options?.checkoutHref,
174
- enableCoupon: options?.enableCoupon,
175
- });
176
- }, [
177
- cart,
178
- options?.freeShippingTarget,
179
- options?.locale,
180
- options?.checkoutHref,
181
- options?.enableCoupon,
182
- ]);
183
-
184
- const addItems = useMutation({
185
- mutationFn: (items: Array<{ id: string; quantity: number; seller: string }>) => {
186
- const orderFormId = query.data?.orderFormId;
187
- if (!orderFormId) throw new Error("Cart not loaded");
188
- return addItemsToCart(orderFormId, items);
189
- },
190
- onSuccess: (data) => {
191
- queryClient.setQueryData(CART_QUERY_KEY, data);
192
- },
193
- });
194
-
195
- const updateQuantity = useMutation({
196
- mutationFn: ({ index, quantity }: { index: number; quantity: number }) => {
197
- const orderFormId = query.data?.orderFormId;
198
- if (!orderFormId) throw new Error("Cart not loaded");
199
- return updateItemQuantity(orderFormId, index, quantity);
200
- },
201
- onSuccess: (data) => {
202
- queryClient.setQueryData(CART_QUERY_KEY, data);
203
- },
204
- });
205
-
206
- const removeItem = useMutation({
207
- mutationFn: (index: number) => {
208
- const orderFormId = query.data?.orderFormId;
209
- if (!orderFormId) throw new Error("Cart not loaded");
210
- return updateItemQuantity(orderFormId, index, 0);
211
- },
212
- onSuccess: (data) => {
213
- queryClient.setQueryData(CART_QUERY_KEY, data);
214
- },
215
- });
216
-
217
- const addCoupons = useMutation({
218
- mutationFn: (text: string) => {
219
- const orderFormId = query.data?.orderFormId;
220
- if (!orderFormId) throw new Error("Cart not loaded");
221
- return addCouponsToCart(orderFormId, text);
222
- },
223
- onSuccess: (data) => {
224
- queryClient.setQueryData(CART_QUERY_KEY, data);
225
- },
226
- });
227
-
228
- return {
229
- /** Raw VTEX OrderForm — escape hatch for platform-specific reads. */
230
- cart,
231
- /** Canonical platform-agnostic minicart. Prefer this in new UI code. */
232
- minicart,
233
- isLoading: query.isLoading,
234
- isError: query.isError,
235
- error: query.error,
236
- refetch: query.refetch,
237
- addItems,
238
- addCoupons,
239
- updateQuantity,
240
- removeItem,
241
- itemCount: cart?.items?.length ?? 0,
242
- };
243
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * Client-side user/session hook for VTEX.
3
- *
4
- * Detects login state via the VTEX Sessions API (server-side accessible).
5
- * Does NOT attempt to read VtexIdclientAutCookie client-side — that cookie
6
- * is HttpOnly and inaccessible via document.cookie.
7
- *
8
- * @example
9
- * ```tsx
10
- * import { useUser } from "@decocms/apps/vtex/hooks/useUser";
11
- *
12
- * function UserGreeting() {
13
- * const { user, isLoggedIn } = useUser();
14
- * if (!isLoggedIn) return <a href="/account">Sign In</a>;
15
- * return <span>Hello, {user?.email}</span>;
16
- * }
17
- * ```
18
- */
19
-
20
- import { useQuery } from "@tanstack/react-query";
21
-
22
- export interface VtexUser {
23
- email?: string;
24
- firstName?: string;
25
- lastName?: string;
26
- userId?: string;
27
- isLoggedIn: boolean;
28
- }
29
-
30
- const USER_QUERY_KEY = ["vtex", "user"] as const;
31
-
32
- async function fetchUser(): Promise<VtexUser> {
33
- try {
34
- const res = await fetch(
35
- "/api/sessions?items=profile.email,profile.firstName,profile.lastName,profile.id",
36
- { credentials: "include" },
37
- );
38
- if (!res.ok) return { isLoggedIn: false };
39
-
40
- const data = await res.json();
41
- const profile = data?.namespaces?.profile;
42
-
43
- const email = profile?.email?.value;
44
- if (!email) return { isLoggedIn: false };
45
-
46
- return {
47
- email,
48
- firstName: profile?.firstName?.value,
49
- lastName: profile?.lastName?.value,
50
- userId: profile?.id?.value,
51
- isLoggedIn: true,
52
- };
53
- } catch {
54
- return { isLoggedIn: false };
55
- }
56
- }
57
-
58
- export interface UseUserOptions {
59
- enabled?: boolean;
60
- staleTime?: number;
61
- }
62
-
63
- export function useUser(options?: UseUserOptions) {
64
- const query = useQuery({
65
- queryKey: USER_QUERY_KEY,
66
- queryFn: fetchUser,
67
- staleTime: options?.staleTime ?? 30_000,
68
- enabled: options?.enabled !== false,
69
- });
70
-
71
- return {
72
- user: query.data ?? null,
73
- isLoggedIn: query.data?.isLoggedIn ?? false,
74
- isLoading: query.isLoading,
75
- isError: query.isError,
76
- refetch: query.refetch,
77
- };
78
- }