@decocms/apps-vtex 7.2.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 +67 -0
- package/src/README.md +6 -0
- package/src/__tests__/client-segment-cookie.test.ts +255 -0
- package/src/__tests__/client-set-cookie-forward.test.ts +257 -0
- package/src/actions/address.ts +250 -0
- package/src/actions/analytics/sendEvent.ts +86 -0
- package/src/actions/auth.ts +333 -0
- package/src/actions/checkout.ts +522 -0
- package/src/actions/index.ts +11 -0
- package/src/actions/masterData.ts +168 -0
- package/src/actions/misc.ts +188 -0
- package/src/actions/newsletter.ts +105 -0
- package/src/actions/orders.ts +34 -0
- package/src/actions/profile.ts +201 -0
- package/src/actions/session.ts +85 -0
- package/src/actions/trigger.ts +43 -0
- package/src/actions/wishlist.ts +114 -0
- package/src/client.ts +667 -0
- package/src/commerceLoaders.ts +261 -0
- package/src/hooks/__tests__/createUseCart.test.ts +184 -0
- package/src/hooks/__tests__/createUseUser.test.ts +48 -0
- package/src/hooks/__tests__/createUseWishlist.test.ts +87 -0
- package/src/hooks/createUseCart.ts +360 -0
- package/src/hooks/createUseUser.ts +153 -0
- package/src/hooks/createUseWishlist.ts +242 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useAutocomplete.ts +83 -0
- package/src/hooks/useCart.ts +243 -0
- package/src/hooks/useUser.ts +78 -0
- package/src/hooks/useWishlist.ts +119 -0
- package/src/index.ts +17 -0
- package/src/invoke.ts +181 -0
- package/src/loaders/ProductDetailsPage.ts +1 -0
- package/src/loaders/ProductList.ts +1 -0
- package/src/loaders/ProductListingPage.ts +1 -0
- package/src/loaders/address.ts +115 -0
- package/src/loaders/autocomplete.ts +58 -0
- package/src/loaders/brands.ts +44 -0
- package/src/loaders/cart.ts +52 -0
- package/src/loaders/catalog.ts +163 -0
- package/src/loaders/collections.ts +55 -0
- package/src/loaders/index.ts +19 -0
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +20 -0
- package/src/loaders/intelligentSearch/productDetailsPage.ts +95 -0
- package/src/loaders/intelligentSearch/productList.ts +154 -0
- package/src/loaders/intelligentSearch/productListingPage.ts +506 -0
- package/src/loaders/intelligentSearch/suggestions.ts +46 -0
- package/src/loaders/legacy/productDetailsPage.ts +1 -0
- package/src/loaders/legacy/productList.ts +1 -0
- package/src/loaders/legacy/relatedProductsLoader.ts +81 -0
- package/src/loaders/legacy.ts +635 -0
- package/src/loaders/logistics.ts +111 -0
- package/src/loaders/minicart.ts +78 -0
- package/src/loaders/navbar.ts +26 -0
- package/src/loaders/orders.ts +92 -0
- package/src/loaders/pageType.ts +68 -0
- package/src/loaders/payment.ts +97 -0
- package/src/loaders/productListFull.ts +159 -0
- package/src/loaders/profile.ts +138 -0
- package/src/loaders/promotion.ts +30 -0
- package/src/loaders/search.ts +124 -0
- package/src/loaders/session.ts +83 -0
- package/src/loaders/user.ts +87 -0
- package/src/loaders/wishlist.ts +89 -0
- package/src/loaders/wishlistProducts.ts +69 -0
- package/src/loaders/workflow/products.ts +64 -0
- package/src/loaders/workflow.ts +316 -0
- package/src/logo.png +0 -0
- package/src/middleware.ts +240 -0
- package/src/mod.ts +152 -0
- package/src/registry.ts +9 -0
- package/src/types.ts +248 -0
- package/src/utils/__tests__/cookieSanitizer.test.ts +162 -0
- package/src/utils/__tests__/fetch.test.ts +80 -0
- package/src/utils/__tests__/fetchCache.test.ts +112 -0
- package/src/utils/__tests__/instrumentedFetch.test.ts +158 -0
- package/src/utils/__tests__/intelligentSearch.test.ts +88 -0
- package/src/utils/__tests__/minicart.test.ts +184 -0
- package/src/utils/__tests__/operationRouter.test.ts +227 -0
- package/src/utils/__tests__/resourceRange.test.ts +32 -0
- package/src/utils/__tests__/sitemap.test.ts +185 -0
- package/src/utils/__tests__/slugify.test.ts +31 -0
- package/src/utils/__tests__/transform.test.ts +698 -0
- package/src/utils/accountLoaders.ts +203 -0
- package/src/utils/authHelpers.ts +85 -0
- package/src/utils/batch.ts +18 -0
- package/src/utils/cookieSanitizer.ts +173 -0
- package/src/utils/cookies.ts +167 -0
- package/src/utils/enrichment.ts +560 -0
- package/src/utils/fetch.ts +107 -0
- package/src/utils/fetchCache.ts +222 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/instrumentedFetch.ts +78 -0
- package/src/utils/intelligentSearch.ts +96 -0
- package/src/utils/legacy.ts +139 -0
- package/src/utils/minicart.ts +139 -0
- package/src/utils/operationRouter.ts +139 -0
- package/src/utils/pickAndOmit.ts +25 -0
- package/src/utils/proxy.ts +435 -0
- package/src/utils/resourceRange.ts +10 -0
- package/src/utils/segment.ts +152 -0
- package/src/utils/similars.ts +37 -0
- package/src/utils/sitemap.ts +282 -0
- package/src/utils/slugCache.ts +28 -0
- package/src/utils/slugify.ts +13 -0
- package/src/utils/transform.ts +1509 -0
- package/src/utils/types.ts +1884 -0
- package/src/utils/vtexId.ts +138 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side wishlist hook for VTEX.
|
|
3
|
+
*
|
|
4
|
+
* Reads the wishlist via the invoke proxy and provides
|
|
5
|
+
* add/remove mutations with automatic cache invalidation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { useWishlist } from "@decocms/apps/vtex/hooks/useWishlist";
|
|
10
|
+
*
|
|
11
|
+
* function WishlistButton({ productId, sku }: Props) {
|
|
12
|
+
* const { isInWishlist, toggle, isLoading } = useWishlist();
|
|
13
|
+
* const wishlisted = isInWishlist(productId);
|
|
14
|
+
* return (
|
|
15
|
+
* <button onClick={() => toggle({ productId, sku })} disabled={isLoading}>
|
|
16
|
+
* {wishlisted ? "♥" : "♡"}
|
|
17
|
+
* </button>
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
24
|
+
|
|
25
|
+
export interface WishlistItem {
|
|
26
|
+
id: string;
|
|
27
|
+
productId: string;
|
|
28
|
+
sku: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const WISHLIST_QUERY_KEY = ["vtex", "wishlist"] as const;
|
|
32
|
+
|
|
33
|
+
async function fetchWishlist(): Promise<WishlistItem[]> {
|
|
34
|
+
const res = await fetch("/deco/invoke/vtex/loaders/wishlist", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({}),
|
|
38
|
+
credentials: "include",
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
if (res.status === 401) return [];
|
|
42
|
+
throw new Error(`Wishlist fetch failed: ${res.status}`);
|
|
43
|
+
}
|
|
44
|
+
return res.json();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function addToWishlist(item: { productId: string; sku: string }): Promise<void> {
|
|
48
|
+
const res = await fetch("/deco/invoke/vtex/actions/wishlist/addItem", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify(item),
|
|
52
|
+
credentials: "include",
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) throw new Error(`Add to wishlist failed: ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function removeFromWishlist(id: string): Promise<void> {
|
|
58
|
+
const res = await fetch("/deco/invoke/vtex/actions/wishlist/removeItem", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ id }),
|
|
62
|
+
credentials: "include",
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw new Error(`Remove from wishlist failed: ${res.status}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface UseWishlistOptions {
|
|
68
|
+
enabled?: boolean;
|
|
69
|
+
staleTime?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useWishlist(options?: UseWishlistOptions) {
|
|
73
|
+
const queryClient = useQueryClient();
|
|
74
|
+
|
|
75
|
+
const query = useQuery({
|
|
76
|
+
queryKey: WISHLIST_QUERY_KEY,
|
|
77
|
+
queryFn: fetchWishlist,
|
|
78
|
+
staleTime: options?.staleTime ?? 60_000,
|
|
79
|
+
enabled: options?.enabled !== false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const items = query.data ?? [];
|
|
83
|
+
const productIdSet = new Set(items.map((i) => i.productId));
|
|
84
|
+
|
|
85
|
+
const addMutation = useMutation({
|
|
86
|
+
mutationFn: addToWishlist,
|
|
87
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: WISHLIST_QUERY_KEY }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const removeMutation = useMutation({
|
|
91
|
+
mutationFn: removeFromWishlist,
|
|
92
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: WISHLIST_QUERY_KEY }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function isInWishlist(productId: string): boolean {
|
|
96
|
+
return productIdSet.has(productId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toggle(item: { productId: string; sku: string }) {
|
|
100
|
+
const existing = items.find((i) => i.productId === item.productId);
|
|
101
|
+
if (existing) {
|
|
102
|
+
removeMutation.mutate(existing.id);
|
|
103
|
+
} else {
|
|
104
|
+
addMutation.mutate(item);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
items,
|
|
110
|
+
isLoading: query.isLoading || addMutation.isPending || removeMutation.isPending,
|
|
111
|
+
isError: query.isError,
|
|
112
|
+
isInWishlist,
|
|
113
|
+
toggle,
|
|
114
|
+
add: addMutation,
|
|
115
|
+
remove: removeMutation,
|
|
116
|
+
refetch: query.refetch,
|
|
117
|
+
count: items.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTEX app entry point for @decocms/apps.
|
|
3
|
+
* Re-exports client config + initializer + app contract.
|
|
4
|
+
*
|
|
5
|
+
* For actions/loaders/utils, use sub-path imports:
|
|
6
|
+
* import { addItemsToCart } from "@decocms/apps/vtex/actions/checkout"
|
|
7
|
+
* import { searchProducts } from "@decocms/apps/vtex/loaders/catalog"
|
|
8
|
+
* import { slugify } from "@decocms/apps/vtex/utils/slugify"
|
|
9
|
+
*
|
|
10
|
+
* Or barrel imports:
|
|
11
|
+
* import { addItemsToCart } from "@decocms/apps/vtex/actions"
|
|
12
|
+
* import { searchProducts } from "@decocms/apps/vtex/loaders"
|
|
13
|
+
*/
|
|
14
|
+
export * from "./client";
|
|
15
|
+
export { configure, type VtexState } from "./mod";
|
|
16
|
+
export { type CreateVtexFetchOptions, createVtexFetch } from "./utils/instrumentedFetch";
|
|
17
|
+
export { vtexOperationRouter } from "./utils/operationRouter";
|
package/src/invoke.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed `invoke.vtex.actions.*` object — generator contract.
|
|
3
|
+
*
|
|
4
|
+
* This file is the source of truth that `@decocms/start/scripts/generate-invoke.ts`
|
|
5
|
+
* scans to emit the site-local `src/server/invoke.gen.ts`. The generator:
|
|
6
|
+
*
|
|
7
|
+
* 1. parses the imports in this file to learn which action lives where,
|
|
8
|
+
* 2. walks `invoke.vtex.actions.*` and extracts each entry's:
|
|
9
|
+
* - validated input type (the arrow function's first parameter),
|
|
10
|
+
* - imported action function (matched against the import map by name),
|
|
11
|
+
* - output type (the outermost `as` cast's `Promise<...>` payload),
|
|
12
|
+
* 3. emits a top-level `createServerFn` per action so TanStack Start's
|
|
13
|
+
* compiler can transform `.handler()` into client RPC stubs (the
|
|
14
|
+
* compiler only walks top-level decls, not factory-returned ones).
|
|
15
|
+
*
|
|
16
|
+
* Every action gets a `forwardResponseCookies()` call in the generated
|
|
17
|
+
* handler — that bridges `Set-Cookie` headers captured by
|
|
18
|
+
* `vtexFetchWithCookies` into TanStack Start's HTTP response. Without it,
|
|
19
|
+
* `checkout.vtex.com` and `CheckoutOrderFormOwnership` never reach the
|
|
20
|
+
* browser, and the storefront's mini-cart drifts away from VTEX's
|
|
21
|
+
* server-side orderForm.
|
|
22
|
+
*
|
|
23
|
+
* To add a new action:
|
|
24
|
+
* 1. Add an entry below with the input/output types and the action call,
|
|
25
|
+
* 2. From a site repo: `npm run generate:invoke`.
|
|
26
|
+
*/
|
|
27
|
+
import { createInvokeFn } from "@decocms/tanstack";
|
|
28
|
+
import {
|
|
29
|
+
addCouponToCart,
|
|
30
|
+
addItemsToCart,
|
|
31
|
+
getOrCreateCart,
|
|
32
|
+
getSellersByRegion,
|
|
33
|
+
type RegionResult,
|
|
34
|
+
type SimulationItem,
|
|
35
|
+
setShippingPostalCode,
|
|
36
|
+
simulateCart,
|
|
37
|
+
updateCartItems,
|
|
38
|
+
updateOrderFormAttachment,
|
|
39
|
+
} from "./actions/checkout";
|
|
40
|
+
import {
|
|
41
|
+
type CreateDocumentResult,
|
|
42
|
+
createDocument,
|
|
43
|
+
getDocument,
|
|
44
|
+
patchDocument,
|
|
45
|
+
searchDocuments,
|
|
46
|
+
type UploadAttachmentOpts,
|
|
47
|
+
uploadAttachment,
|
|
48
|
+
} from "./actions/masterData";
|
|
49
|
+
import { type NotifyMeProps, notifyMe } from "./actions/misc";
|
|
50
|
+
import { type SubscribeProps, subscribe } from "./actions/newsletter";
|
|
51
|
+
import { createSession, editSession, type SessionData } from "./actions/session";
|
|
52
|
+
import type { OrderForm } from "./types";
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// invoke.vtex.actions — typed server functions callable from client
|
|
56
|
+
//
|
|
57
|
+
// Action bodies receive the validated input object directly and pass it
|
|
58
|
+
// straight to the action function (which expects a single `props` object).
|
|
59
|
+
// The arrow function body is what the generator parses for "which action
|
|
60
|
+
// is this entry calling" — keep the call site shaped as `actionName(data)`
|
|
61
|
+
// so the matcher in `generate-invoke.ts` picks the right importedFn.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export const invoke = {
|
|
65
|
+
vtex: {
|
|
66
|
+
actions: {
|
|
67
|
+
// -- Cart (OrderForm CRUD) --------------------------------------------
|
|
68
|
+
|
|
69
|
+
getOrCreateCart: createInvokeFn((data: { orderFormId?: string }) =>
|
|
70
|
+
getOrCreateCart(data),
|
|
71
|
+
) as unknown as (ctx: { data: { orderFormId?: string } }) => Promise<OrderForm>,
|
|
72
|
+
|
|
73
|
+
addItemsToCart: createInvokeFn(
|
|
74
|
+
(data: {
|
|
75
|
+
orderFormId: string;
|
|
76
|
+
orderItems: Array<{
|
|
77
|
+
id: string;
|
|
78
|
+
seller: string;
|
|
79
|
+
quantity: number;
|
|
80
|
+
}>;
|
|
81
|
+
}) => addItemsToCart(data),
|
|
82
|
+
) as unknown as (ctx: {
|
|
83
|
+
data: {
|
|
84
|
+
orderFormId: string;
|
|
85
|
+
orderItems: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
seller: string;
|
|
88
|
+
quantity: number;
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
91
|
+
}) => Promise<OrderForm>,
|
|
92
|
+
|
|
93
|
+
updateCartItems: createInvokeFn(
|
|
94
|
+
(data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> }) =>
|
|
95
|
+
updateCartItems(data),
|
|
96
|
+
) as unknown as (ctx: {
|
|
97
|
+
data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> };
|
|
98
|
+
}) => Promise<OrderForm>,
|
|
99
|
+
|
|
100
|
+
addCouponToCart: createInvokeFn((data: { orderFormId: string; text: string }) =>
|
|
101
|
+
addCouponToCart(data),
|
|
102
|
+
) as unknown as (ctx: { data: { orderFormId: string; text: string } }) => Promise<OrderForm>,
|
|
103
|
+
|
|
104
|
+
simulateCart: createInvokeFn(
|
|
105
|
+
(data: { items: SimulationItem[]; postalCode: string; country?: string }) =>
|
|
106
|
+
simulateCart(data),
|
|
107
|
+
),
|
|
108
|
+
|
|
109
|
+
// -- Shipping / Region ------------------------------------------------
|
|
110
|
+
|
|
111
|
+
getSellersByRegion: createInvokeFn((data: { postalCode: string; salesChannel?: string }) =>
|
|
112
|
+
getSellersByRegion(data),
|
|
113
|
+
) as unknown as (ctx: {
|
|
114
|
+
data: { postalCode: string; salesChannel?: string };
|
|
115
|
+
}) => Promise<RegionResult | null>,
|
|
116
|
+
|
|
117
|
+
setShippingPostalCode: createInvokeFn(
|
|
118
|
+
(data: { orderFormId: string; postalCode: string; country?: string }) =>
|
|
119
|
+
setShippingPostalCode(data),
|
|
120
|
+
) as unknown as (ctx: {
|
|
121
|
+
data: { orderFormId: string; postalCode: string; country?: string };
|
|
122
|
+
}) => Promise<boolean>,
|
|
123
|
+
|
|
124
|
+
updateOrderFormAttachment: createInvokeFn(
|
|
125
|
+
(data: { orderFormId: string; attachment: string; body: Record<string, unknown> }) =>
|
|
126
|
+
updateOrderFormAttachment(data),
|
|
127
|
+
) as unknown as (ctx: {
|
|
128
|
+
data: { orderFormId: string; attachment: string; body: Record<string, unknown> };
|
|
129
|
+
}) => Promise<OrderForm>,
|
|
130
|
+
|
|
131
|
+
// -- Session ----------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
createSession: createInvokeFn((data: Record<string, any>) => createSession({ data })),
|
|
134
|
+
|
|
135
|
+
editSession: createInvokeFn((data: { public: Record<string, { value: string }> }) =>
|
|
136
|
+
editSession(data),
|
|
137
|
+
) as unknown as (ctx: {
|
|
138
|
+
data: { public: Record<string, { value: string }> };
|
|
139
|
+
}) => Promise<SessionData>,
|
|
140
|
+
|
|
141
|
+
// -- MasterData -------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
createDocument: createInvokeFn((data: { entity: string; data: Record<string, any> }) =>
|
|
144
|
+
createDocument(data),
|
|
145
|
+
) as unknown as (ctx: {
|
|
146
|
+
data: { entity: string; data: Record<string, any> };
|
|
147
|
+
}) => Promise<CreateDocumentResult>,
|
|
148
|
+
|
|
149
|
+
getDocument: createInvokeFn((data: { entity: string; documentId: string }) =>
|
|
150
|
+
getDocument(data),
|
|
151
|
+
),
|
|
152
|
+
|
|
153
|
+
patchDocument: createInvokeFn(
|
|
154
|
+
(data: { entity: string; documentId: string; data: Record<string, any> }) =>
|
|
155
|
+
patchDocument(data),
|
|
156
|
+
) as unknown as (ctx: {
|
|
157
|
+
data: { entity: string; documentId: string; data: Record<string, any> };
|
|
158
|
+
}) => Promise<void>,
|
|
159
|
+
|
|
160
|
+
searchDocuments: createInvokeFn((data: { entity: string; filter: string }) =>
|
|
161
|
+
searchDocuments(data),
|
|
162
|
+
),
|
|
163
|
+
|
|
164
|
+
uploadAttachment: createInvokeFn((data: UploadAttachmentOpts) =>
|
|
165
|
+
uploadAttachment(data),
|
|
166
|
+
) as unknown as (ctx: { data: UploadAttachmentOpts }) => Promise<{ ok: true }>,
|
|
167
|
+
|
|
168
|
+
// -- Newsletter -------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
subscribe: createInvokeFn((data: SubscribeProps) => subscribe(data)) as unknown as (ctx: {
|
|
171
|
+
data: SubscribeProps;
|
|
172
|
+
}) => Promise<void>,
|
|
173
|
+
|
|
174
|
+
// -- Misc -------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
notifyMe: createInvokeFn((data: NotifyMeProps) => notifyMe(data)) as unknown as (ctx: {
|
|
177
|
+
data: NotifyMeProps;
|
|
178
|
+
}) => Promise<void>,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
} as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type PDPProps } from "./intelligentSearch/productDetailsPage";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type ProductListProps } from "./intelligentSearch/productList";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type PLPProps } from "./intelligentSearch/productListingPage";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTEX Address API loaders.
|
|
3
|
+
* Pure async functions — require configureVtex() to have been called.
|
|
4
|
+
*
|
|
5
|
+
* Ported from deco-cx/apps:
|
|
6
|
+
* vtex/loaders/address/getAddressByPostalCode.ts
|
|
7
|
+
* vtex/loaders/address/getUserAddresses.ts
|
|
8
|
+
*
|
|
9
|
+
* @see https://developers.vtex.com/docs/api-reference/checkout-api
|
|
10
|
+
*/
|
|
11
|
+
import { vtexFetch, vtexIOGraphQL } from "../client";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface PostalAddress {
|
|
18
|
+
"@type": "PostalAddress";
|
|
19
|
+
postalCode?: string;
|
|
20
|
+
addressLocality?: string;
|
|
21
|
+
addressRegion?: string;
|
|
22
|
+
addressCountry?: string;
|
|
23
|
+
streetAddress?: string;
|
|
24
|
+
identifier?: string;
|
|
25
|
+
areaServed?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
disambiguatingDescription?: string;
|
|
28
|
+
latitude?: number;
|
|
29
|
+
longitude?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface VtexAddress {
|
|
33
|
+
addressId: string;
|
|
34
|
+
addressType: string;
|
|
35
|
+
addressName: string;
|
|
36
|
+
city: string;
|
|
37
|
+
complement: string;
|
|
38
|
+
country: string;
|
|
39
|
+
neighborhood: string;
|
|
40
|
+
number: string;
|
|
41
|
+
postalCode: string;
|
|
42
|
+
geoCoordinates: number[];
|
|
43
|
+
receiverName: string;
|
|
44
|
+
state: string;
|
|
45
|
+
street: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// getAddressByPostalCode
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Look up a postal address by country + postal code (public API).
|
|
54
|
+
* @see https://developers.vtex.com/docs/api-reference/checkout-api#get-/api/checkout/pub/postal-code/-countryCode-/-postalCode-
|
|
55
|
+
*/
|
|
56
|
+
export async function getAddressByPostalCode(
|
|
57
|
+
countryCode: string,
|
|
58
|
+
postalCode: string,
|
|
59
|
+
): Promise<PostalAddress> {
|
|
60
|
+
const data = await vtexFetch<Record<string, any>>(
|
|
61
|
+
`/api/checkout/pub/postal-code/${countryCode}/${postalCode}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"@type": "PostalAddress",
|
|
66
|
+
postalCode: data.postalCode,
|
|
67
|
+
addressLocality: data.city,
|
|
68
|
+
addressRegion: data.state,
|
|
69
|
+
addressCountry: data.country,
|
|
70
|
+
streetAddress: data.street || undefined,
|
|
71
|
+
identifier: data.number || undefined,
|
|
72
|
+
areaServed: data.neighborhood || undefined,
|
|
73
|
+
description: data.complement || undefined,
|
|
74
|
+
disambiguatingDescription: data.reference || undefined,
|
|
75
|
+
latitude: data.geoCoordinates?.[0] ?? undefined,
|
|
76
|
+
longitude: data.geoCoordinates?.[1] ?? undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// getUserAddresses (authenticated — VTEX IO GraphQL)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const USER_ADDRESSES_QUERY = `query Addresses @context(scope: "private") {
|
|
85
|
+
profile {
|
|
86
|
+
cacheId
|
|
87
|
+
addresses {
|
|
88
|
+
addressId: id
|
|
89
|
+
addressType
|
|
90
|
+
addressName
|
|
91
|
+
city
|
|
92
|
+
complement
|
|
93
|
+
country
|
|
94
|
+
neighborhood
|
|
95
|
+
number
|
|
96
|
+
postalCode
|
|
97
|
+
geoCoordinates
|
|
98
|
+
receiverName
|
|
99
|
+
state
|
|
100
|
+
street
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}`;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fetch addresses for the currently authenticated user.
|
|
107
|
+
* Requires a valid VTEX auth cookie.
|
|
108
|
+
*/
|
|
109
|
+
export async function getUserAddresses(authCookie: string): Promise<VtexAddress[]> {
|
|
110
|
+
const { profile } = await vtexIOGraphQL<{
|
|
111
|
+
profile: { addresses: VtexAddress[] };
|
|
112
|
+
}>({ query: USER_ADDRESSES_QUERY }, { cookie: authCookie });
|
|
113
|
+
|
|
114
|
+
return profile?.addresses ?? [];
|
|
115
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTEX Intelligent Search autocomplete — suggestions + product results.
|
|
3
|
+
*
|
|
4
|
+
* Combines /autocomplete_suggestions/ and /product_search/ in parallel,
|
|
5
|
+
* transforms IS products to schema.org via pickSku + toProduct.
|
|
6
|
+
*/
|
|
7
|
+
import { getVtexConfig, intelligentSearch as vtexIS } from "../client";
|
|
8
|
+
import { pickSku, toProduct as toSchemaProduct } from "../utils/transform";
|
|
9
|
+
|
|
10
|
+
export interface AutocompleteProps {
|
|
11
|
+
query: string;
|
|
12
|
+
count?: number;
|
|
13
|
+
showSponsored?: boolean;
|
|
14
|
+
placement?: string;
|
|
15
|
+
fuzzy?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AutocompleteResult {
|
|
19
|
+
searches: Array<{ term: string; count: number; attributes?: any[] }>;
|
|
20
|
+
products: any[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function autocompleteSearch(props: AutocompleteProps): Promise<AutocompleteResult> {
|
|
24
|
+
const query = props.query || "";
|
|
25
|
+
const count = props.count ?? 4;
|
|
26
|
+
if (!query.trim()) return { searches: [], products: [] };
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const [suggestionsData, productsData] = await Promise.all([
|
|
30
|
+
vtexIS<{
|
|
31
|
+
searches: Array<{ term: string; count: number; attributes?: any[] }>;
|
|
32
|
+
}>("/autocomplete_suggestions/", { query }),
|
|
33
|
+
vtexIS<{ products: any[] }>("/product_search/", {
|
|
34
|
+
query,
|
|
35
|
+
count: String(count),
|
|
36
|
+
showSponsored: props.showSponsored !== false ? "true" : "false",
|
|
37
|
+
placement: props.placement ?? "top-search",
|
|
38
|
+
fuzzy: props.fuzzy ?? "0",
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const config = getVtexConfig();
|
|
43
|
+
const baseUrl = config.publicUrl
|
|
44
|
+
? `https://${config.publicUrl}`
|
|
45
|
+
: `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
searches: suggestionsData.searches ?? [],
|
|
49
|
+
products: (productsData.products ?? []).slice(0, count).map((p: any) => {
|
|
50
|
+
const sku = pickSku(p);
|
|
51
|
+
return toSchemaProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("[vtex] autocompleteSearch error:", error);
|
|
56
|
+
return { searches: [], products: [] };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTEX brand loaders.
|
|
3
|
+
* Returns schema.org Brand[] matching the original deco-cx/apps format.
|
|
4
|
+
*
|
|
5
|
+
* @see https://developers.vtex.com/docs/api-reference/catalog-api#get-/api/catalog_system/pub/brand/list
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Brand } from "@decocms/apps-commerce/types";
|
|
9
|
+
import { getVtexConfig, vtexFetch } from "../client";
|
|
10
|
+
import { toBrand } from "../utils/transform";
|
|
11
|
+
import type { Brand as BrandVTEX } from "../utils/types";
|
|
12
|
+
|
|
13
|
+
export interface ListBrandsOpts {
|
|
14
|
+
/** When true, only returns active brands. @default false */
|
|
15
|
+
filterInactive?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List brands from the VTEX catalog, transformed to schema.org Brand format.
|
|
20
|
+
*/
|
|
21
|
+
export async function listBrands(opts?: ListBrandsOpts): Promise<Brand[]> {
|
|
22
|
+
const config = getVtexConfig();
|
|
23
|
+
const baseUrl = `https://${config.account}.vteximg.com.br/arquivos/ids`;
|
|
24
|
+
|
|
25
|
+
const brands = await vtexFetch<BrandVTEX[]>("/api/catalog_system/pub/brand/list");
|
|
26
|
+
|
|
27
|
+
const filtered = opts?.filterInactive ? brands.filter((b) => b.isActive) : brands;
|
|
28
|
+
|
|
29
|
+
return filtered.map((b) => toBrand(b, baseUrl));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get a single brand by ID, as schema.org Brand.
|
|
34
|
+
*/
|
|
35
|
+
export async function getBrandById(brandId: number): Promise<Brand | null> {
|
|
36
|
+
try {
|
|
37
|
+
const config = getVtexConfig();
|
|
38
|
+
const baseUrl = `https://${config.account}.vteximg.com.br/arquivos/ids`;
|
|
39
|
+
const brand = await vtexFetch<BrandVTEX>(`/api/catalog_system/pub/brand/${brandId}`);
|
|
40
|
+
return toBrand(brand, baseUrl);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTEX Cart (OrderForm) loader.
|
|
3
|
+
* Pure async function — requires configureVtex() to have been called.
|
|
4
|
+
*
|
|
5
|
+
* Ported from deco-cx/apps:
|
|
6
|
+
* vtex/loaders/cart.ts
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.vtex.com/docs/api-reference/checkout-api#get-/api/checkout/pub/orderForm
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DEFAULT_EXPECTED_SECTIONS } from "../actions/checkout";
|
|
12
|
+
import { getVtexConfig, vtexFetch } from "../client";
|
|
13
|
+
import { forceHttpsOnAssets } from "../utils/transform";
|
|
14
|
+
import type { OrderForm } from "../utils/types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the current cart (OrderForm).
|
|
18
|
+
*
|
|
19
|
+
* When `orderFormId` is provided the existing cart is retrieved;
|
|
20
|
+
* otherwise a fresh OrderForm is created via POST.
|
|
21
|
+
*
|
|
22
|
+
* @param orderFormId - Optional existing orderForm ID (from checkout cookie)
|
|
23
|
+
* @param salesChannel - Optional sales channel override
|
|
24
|
+
* @param authCookie - Optional cookie string for authenticated requests
|
|
25
|
+
*/
|
|
26
|
+
export async function getCart(
|
|
27
|
+
orderFormId?: string,
|
|
28
|
+
opts?: { salesChannel?: string; authCookie?: string },
|
|
29
|
+
): Promise<OrderForm> {
|
|
30
|
+
const { salesChannel } = getVtexConfig();
|
|
31
|
+
const sc = opts?.salesChannel ?? salesChannel;
|
|
32
|
+
const headers: Record<string, string> = {};
|
|
33
|
+
if (opts?.authCookie) headers.cookie = opts.authCookie;
|
|
34
|
+
|
|
35
|
+
const scParam = sc ? `?sc=${sc}` : "";
|
|
36
|
+
|
|
37
|
+
const body = JSON.stringify({ expectedOrderFormSections: DEFAULT_EXPECTED_SECTIONS });
|
|
38
|
+
|
|
39
|
+
const cart = orderFormId
|
|
40
|
+
? await vtexFetch<OrderForm>(`/api/checkout/pub/orderForm/${orderFormId}${scParam}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body,
|
|
44
|
+
})
|
|
45
|
+
: await vtexFetch<OrderForm>(`/api/checkout/pub/orderForm${scParam}`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers,
|
|
48
|
+
body,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return forceHttpsOnAssets(cart);
|
|
52
|
+
}
|