@decocms/apps 1.7.0 → 1.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
@@ -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: any };
40
+ export type AddItemToCartMutationVariables = { cartId: string; lines: unknown };
41
41
  export type UpdateItemsMutation = { cart?: CartFragment | null };
42
- export type UpdateItemsMutationVariables = { cartId: string; lines: any };
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
- export type ProductFragment = any;
48
- export type ProductVariantFragment = any;
49
- export type GetProductQuery = { product?: any };
50
- export type GetProductQueryVariables = { handle?: string; identifiers?: any[] };
51
- export type ProductRecommendationsQuery = { productRecommendations?: any[] };
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
- export type ProductFilter = any;
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?: any;
69
- addresses?: { nodes: any[] };
70
- orders?: { nodes: any[] };
84
+ defaultAddress?: unknown;
85
+ addresses?: { nodes: unknown[] };
86
+ orders?: { nodes: unknown[] };
71
87
  };
72
88
 
73
89
  export type CustomerAccessTokenCreateInput = {
@@ -31,9 +31,7 @@ import type { OrderForm, OrderFormItem } from "../types";
31
31
  export interface CreateUseCartInvoke {
32
32
  vtex: {
33
33
  actions: {
34
- getOrCreateCart: (args: {
35
- data: { orderFormId?: string };
36
- }) => Promise<OrderForm>;
34
+ getOrCreateCart: (args: { data: { orderFormId?: string } }) => Promise<OrderForm>;
37
35
  addItemsToCart: (args: {
38
36
  data: {
39
37
  orderFormId: string;
@@ -130,10 +128,7 @@ export function createUseCart(opts: CreateUseCartOptions) {
130
128
  return of.orderFormId;
131
129
  }
132
130
 
133
- function itemToAnalyticsItem(
134
- item: OrderFormItem & { coupon?: string },
135
- index: number,
136
- ) {
131
+ function itemToAnalyticsItem(item: OrderFormItem & { coupon?: string }, index: number) {
137
132
  return {
138
133
  item_id: item.productId,
139
134
  item_group_id: item.productId,
@@ -217,11 +212,7 @@ export function createUseCart(opts: CreateUseCartOptions) {
217
212
  },
218
213
  },
219
214
 
220
- addItem: async (params: {
221
- id: string;
222
- seller: string;
223
- quantity?: number;
224
- }) => {
215
+ addItem: async (params: { id: string; seller: string; quantity?: number }) => {
225
216
  setLoading(true);
226
217
  try {
227
218
  const ofId = await ensureOrderForm();
@@ -266,9 +257,7 @@ export function createUseCart(opts: CreateUseCartOptions) {
266
257
  }
267
258
  },
268
259
 
269
- updateItems: async (params: {
270
- orderItems: Array<{ index: number; quantity: number }>;
271
- }) => {
260
+ updateItems: async (params: { orderItems: Array<{ index: number; quantity: number }> }) => {
272
261
  const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
273
262
  if (!ofId) return;
274
263
  setLoading(true);
@@ -319,10 +308,7 @@ export function createUseCart(opts: CreateUseCartOptions) {
319
308
  }
320
309
  },
321
310
 
322
- sendAttachment: async (params: {
323
- attachment: string;
324
- body: Record<string, unknown>;
325
- }) => {
311
+ sendAttachment: async (params: { attachment: string; body: Record<string, unknown> }) => {
326
312
  const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
327
313
  if (!ofId) return;
328
314
  setLoading(true);
@@ -361,9 +347,7 @@ export function createUseCart(opts: CreateUseCartOptions) {
361
347
  },
362
348
 
363
349
  mapItemsToAnalyticsItems: (orderForm: OrderForm | null) => {
364
- return (orderForm?.items || []).map((item, index) =>
365
- itemToAnalyticsItem(item, index),
366
- );
350
+ return (orderForm?.items || []).map((item, index) => itemToAnalyticsItem(item, index));
367
351
  },
368
352
  };
369
353
  }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Factory for the legacy invoke-based `useUser` 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
+ * - signal-shaped accessors (`user.value`, `loading.value`)
8
+ * - awaitable refresh (`await refresh()`)
9
+ *
10
+ * It is intentionally separate from the canonical `useUser` in
11
+ * `vtex/hooks/useUser.ts`, which is built on TanStack Query and exposes
12
+ * `{ user, isLoggedIn, isLoading, refetch }`. Both can coexist in a single site.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // src/hooks/useUser.ts
17
+ * import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser";
18
+ * import { invoke } from "~/server/invoke";
19
+ *
20
+ * export const { useUser, resetUser } = createUseUser({ invoke });
21
+ * export type { Person } from "@decocms/apps/vtex/loaders/user";
22
+ * ```
23
+ */
24
+
25
+ import { useEffect, useState } from "react";
26
+ import type { Person } from "../loaders/user";
27
+
28
+ /** Minimal structural shape of the invoke proxy this hook needs. */
29
+ export interface CreateUseUserInvoke {
30
+ vtex: {
31
+ loaders: {
32
+ user: () => Promise<Person | null>;
33
+ };
34
+ };
35
+ }
36
+
37
+ export interface CreateUseUserOptions {
38
+ invoke: CreateUseUserInvoke;
39
+ }
40
+
41
+ /** Build a per-site `useUser` plus its companions. */
42
+ export function createUseUser(opts: CreateUseUserOptions) {
43
+ const { invoke } = opts;
44
+
45
+ let _user: Person | null = null;
46
+ let _loading = false;
47
+ let _initStarted = false;
48
+ let _initFailed = false;
49
+ const _listeners = new Set<() => void>();
50
+
51
+ function notify() {
52
+ for (const fn of _listeners) fn();
53
+ }
54
+ function setUser(u: Person | null) {
55
+ _user = u;
56
+ notify();
57
+ }
58
+ function setLoading(v: boolean) {
59
+ _loading = v;
60
+ notify();
61
+ }
62
+
63
+ async function refresh(): Promise<Person | null> {
64
+ setLoading(true);
65
+ try {
66
+ const u = await invoke.vtex.loaders.user();
67
+ setUser(u);
68
+ _initFailed = false;
69
+ return u;
70
+ } catch (err) {
71
+ console.error("[useUser] refresh failed:", err);
72
+ _initFailed = true;
73
+ notify();
74
+ return null;
75
+ } finally {
76
+ setLoading(false);
77
+ }
78
+ }
79
+
80
+ /** Reset module-level user state so the next useUser() re-fetches. */
81
+ function resetUser() {
82
+ _user = null;
83
+ _loading = false;
84
+ _initStarted = false;
85
+ _initFailed = false;
86
+ notify();
87
+ }
88
+
89
+ function useUser() {
90
+ const [, forceRender] = useState(0);
91
+
92
+ useEffect(() => {
93
+ const listener = () => forceRender((n) => n + 1);
94
+ _listeners.add(listener);
95
+
96
+ if (!_user && !_initStarted) {
97
+ _initStarted = true;
98
+ setLoading(true);
99
+ invoke.vtex.loaders
100
+ .user()
101
+ .then((u) => {
102
+ setUser(u);
103
+ })
104
+ .catch((err: unknown) => {
105
+ console.error("[useUser] init failed:", err);
106
+ _initFailed = true;
107
+ notify();
108
+ })
109
+ .finally(() => setLoading(false));
110
+ }
111
+
112
+ return () => {
113
+ _listeners.delete(listener);
114
+ };
115
+ }, []);
116
+
117
+ return {
118
+ user: {
119
+ get value() {
120
+ return _user;
121
+ },
122
+ set value(v: Person | null) {
123
+ setUser(v);
124
+ },
125
+ },
126
+
127
+ loading: {
128
+ get value() {
129
+ return _loading;
130
+ },
131
+ },
132
+
133
+ isLoggedIn: {
134
+ get value() {
135
+ return !!_user?.email;
136
+ },
137
+ },
138
+
139
+ initFailed: {
140
+ get value() {
141
+ return _initFailed;
142
+ },
143
+ },
144
+
145
+ refresh,
146
+ };
147
+ }
148
+
149
+ return {
150
+ useUser,
151
+ resetUser,
152
+ };
153
+ }
@@ -0,0 +1,242 @@
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,9 +1,19 @@
1
- export { type UseAutocompleteOptions, useAutocomplete } from "./useAutocomplete";
2
- export { type CartItem, type OrderForm, type UseCartOptions, useCart } from "./useCart";
3
1
  export {
4
2
  type CreateUseCartInvoke,
5
3
  type CreateUseCartOptions,
6
4
  createUseCart,
7
5
  } from "./createUseCart";
6
+ export {
7
+ type CreateUseUserInvoke,
8
+ type CreateUseUserOptions,
9
+ createUseUser,
10
+ } from "./createUseUser";
11
+ export {
12
+ type CreateUseWishlistInvoke,
13
+ type CreateUseWishlistOptions,
14
+ createUseWishlist,
15
+ } from "./createUseWishlist";
16
+ export { type UseAutocompleteOptions, useAutocomplete } from "./useAutocomplete";
17
+ export { type CartItem, type OrderForm, type UseCartOptions, useCart } from "./useCart";
8
18
  export { type UseUserOptions, useUser, type VtexUser } from "./useUser";
9
19
  export { type UseWishlistOptions, useWishlist, type WishlistItem } from "./useWishlist";