@decocms/apps 1.7.1 → 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.1",
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": {
@@ -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
+ }
@@ -3,6 +3,16 @@ export {
3
3
  type CreateUseCartOptions,
4
4
  createUseCart,
5
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";
6
16
  export { type UseAutocompleteOptions, useAutocomplete } from "./useAutocomplete";
7
17
  export { type CartItem, type OrderForm, type UseCartOptions, useCart } from "./useCart";
8
18
  export { type UseUserOptions, useUser, type VtexUser } from "./useUser";