@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 +1 -1
- package/vtex/hooks/createUseUser.ts +153 -0
- package/vtex/hooks/createUseWishlist.ts +242 -0
- package/vtex/hooks/index.ts +10 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/vtex/hooks/index.ts
CHANGED
|
@@ -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";
|