@decocms/apps-nuvemshop 7.74.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 (35) hide show
  1. package/package.json +48 -0
  2. package/src/__fixtures__/browse-category.json +813 -0
  3. package/src/__fixtures__/categories.json +60 -0
  4. package/src/__fixtures__/checkout.json +3 -0
  5. package/src/__fixtures__/product-custom-fields.json +199 -0
  6. package/src/__fixtures__/product.json +313 -0
  7. package/src/__fixtures__/search.json +636 -0
  8. package/src/__fixtures__/shipping-options.json +9 -0
  9. package/src/__tests__/client.test.ts +118 -0
  10. package/src/__tests__/loaders.test.ts +220 -0
  11. package/src/__tests__/mod.test.ts +51 -0
  12. package/src/actions/createCheckout.ts +36 -0
  13. package/src/client.ts +165 -0
  14. package/src/hooks/__tests__/useCart.test.ts +81 -0
  15. package/src/hooks/useCart.ts +115 -0
  16. package/src/index.ts +23 -0
  17. package/src/loaders/categories.ts +27 -0
  18. package/src/loaders/productDetailsPage.ts +30 -0
  19. package/src/loaders/productList.ts +49 -0
  20. package/src/loaders/productListingPage.ts +94 -0
  21. package/src/loaders/relatedProducts.ts +41 -0
  22. package/src/loaders/shippingOptions.ts +29 -0
  23. package/src/loaders/suggestions.ts +24 -0
  24. package/src/manifest.gen.ts +31 -0
  25. package/src/mod.ts +42 -0
  26. package/src/registry.ts +9 -0
  27. package/src/utils/__tests__/listing.test.ts +167 -0
  28. package/src/utils/__tests__/transform.test.ts +212 -0
  29. package/src/utils/instrumentedFetch.ts +40 -0
  30. package/src/utils/listing.ts +183 -0
  31. package/src/utils/operationRouter.ts +21 -0
  32. package/src/utils/request.ts +17 -0
  33. package/src/utils/transform.ts +231 -0
  34. package/src/utils/types.ts +141 -0
  35. package/tsconfig.json +7 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Client-side cart for the Nuvemshop Storefront API.
3
+ *
4
+ * The API has no server cart — only `POST /checkouts`, which opens a core
5
+ * cart from line items and returns the hosted checkout URL. So the cart
6
+ * lives in `localStorage` (shared across tabs) and `checkout()` hands it to
7
+ * the `createCheckout` action, which then redirects.
8
+ *
9
+ * Prices/totals aren't stored: render them from the products you already
10
+ * have; the hosted checkout re-validates stock, prices and the coupon.
11
+ *
12
+ * ```tsx
13
+ * const { items, count, addItem, checkout } = useCart();
14
+ * <button onClick={() => addItem({ productId, variantId, quantity: 1 })}>Comprar</button>
15
+ * ```
16
+ */
17
+ import { useSyncExternalStore } from "react";
18
+ import type { CartItem } from "../actions/createCheckout";
19
+
20
+ export type { CartItem };
21
+
22
+ export const CART_STORAGE_KEY = "nuvemshop:cart";
23
+ /** The API sums repeated variants up to 99. */
24
+ const MAX_QTY = 99;
25
+
26
+ export const addItem = (items: CartItem[], item: CartItem): CartItem[] => {
27
+ const existing = items.find((i) => i.variantId === item.variantId);
28
+ if (!existing) return [...items, { ...item, quantity: Math.min(item.quantity, MAX_QTY) }];
29
+ return setQuantity(items, item.variantId, existing.quantity + item.quantity);
30
+ };
31
+
32
+ export const setQuantity = (items: CartItem[], variantId: number, quantity: number): CartItem[] =>
33
+ quantity <= 0
34
+ ? removeItem(items, variantId)
35
+ : items.map((i) =>
36
+ i.variantId === variantId ? { ...i, quantity: Math.min(quantity, MAX_QTY) } : i,
37
+ );
38
+
39
+ export const removeItem = (items: CartItem[], variantId: number): CartItem[] =>
40
+ items.filter((i) => i.variantId !== variantId);
41
+
42
+ const EMPTY: CartItem[] = [];
43
+
44
+ function read(): CartItem[] {
45
+ try {
46
+ const parsed = JSON.parse(localStorage.getItem(CART_STORAGE_KEY) ?? "[]");
47
+ return Array.isArray(parsed) ? parsed : EMPTY;
48
+ } catch {
49
+ return EMPTY;
50
+ }
51
+ }
52
+
53
+ function createCartStore() {
54
+ let items: CartItem[] | null = null;
55
+ const listeners = new Set<() => void>();
56
+ const notify = () => {
57
+ for (const l of listeners) l();
58
+ };
59
+
60
+ if (typeof window !== "undefined") {
61
+ window.addEventListener("storage", (e) => {
62
+ if (e.key !== CART_STORAGE_KEY) return;
63
+ items = read();
64
+ notify();
65
+ });
66
+ }
67
+
68
+ const store = {
69
+ get: (): CartItem[] => (items ??= typeof localStorage === "undefined" ? EMPTY : read()),
70
+ set(next: CartItem[]) {
71
+ items = next;
72
+ if (typeof localStorage !== "undefined")
73
+ localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(next));
74
+ notify();
75
+ },
76
+ update: (fn: (items: CartItem[]) => CartItem[]) => store.set(fn(store.get())),
77
+ subscribe(listener: () => void) {
78
+ listeners.add(listener);
79
+ return () => listeners.delete(listener);
80
+ },
81
+ };
82
+ return store;
83
+ }
84
+
85
+ export const cartStore = createCartStore();
86
+
87
+ /** Creates the Nuvemshop checkout for `items` and navigates to it. */
88
+ export async function checkout(
89
+ items: CartItem[],
90
+ coupon?: string,
91
+ navigate: (url: string) => void = (url) => window.location.assign(url),
92
+ ) {
93
+ const res = await fetch("/deco/invoke/nuvemshop/actions/createCheckout", {
94
+ method: "POST",
95
+ headers: { "content-type": "application/json" },
96
+ body: JSON.stringify({ items, coupon }),
97
+ });
98
+ if (!res.ok) throw new Error(`Checkout failed (${res.status}): ${await res.text()}`);
99
+ const { checkoutUrl } = (await res.json()) as { checkoutUrl: string };
100
+ navigate(checkoutUrl);
101
+ }
102
+
103
+ export function useCart() {
104
+ const items = useSyncExternalStore(cartStore.subscribe, cartStore.get, () => EMPTY);
105
+ return {
106
+ items,
107
+ count: items.reduce((n, i) => n + i.quantity, 0),
108
+ addItem: (item: CartItem) => cartStore.update((c) => addItem(c, item)),
109
+ setQuantity: (variantId: number, quantity: number) =>
110
+ cartStore.update((c) => setQuantity(c, variantId, quantity)),
111
+ removeItem: (variantId: number) => cartStore.update((c) => removeItem(c, variantId)),
112
+ clear: () => cartStore.set([]),
113
+ checkout: (coupon?: string) => checkout(items, coupon),
114
+ };
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export { default as createCheckout } from "./actions/createCheckout";
2
+ export {
3
+ clearNuvemshopCache,
4
+ configureNuvemshop,
5
+ getNuvemshopConfig,
6
+ type NuvemshopConfig,
7
+ nuvemshopGet,
8
+ nuvemshopPost,
9
+ PRODUCT_FIELDS,
10
+ setNuvemshopFetch,
11
+ } from "./client";
12
+ export { default as categories } from "./loaders/categories";
13
+ export { default as productDetailsPage } from "./loaders/productDetailsPage";
14
+ export { default as productList } from "./loaders/productList";
15
+ export { default as productListingPage } from "./loaders/productListingPage";
16
+ export { default as relatedProducts } from "./loaders/relatedProducts";
17
+ export { default as shippingOptions } from "./loaders/shippingOptions";
18
+ export { default as suggestions } from "./loaders/suggestions";
19
+ export { configure } from "./mod";
20
+ export { NUVEMSHOP_REGISTRY_ENTRY } from "./registry";
21
+ export { createNuvemshopFetch } from "./utils/instrumentedFetch";
22
+ export { nuvemshopOperationRouter } from "./utils/operationRouter";
23
+ export type * from "./utils/types";
@@ -0,0 +1,27 @@
1
+ import type { SiteNavigationElement } from "@decocms/apps-commerce/types";
2
+ import { nuvemshopGet } from "../client";
3
+ import { originOf, type PageProps } from "../utils/request";
4
+ import { categoryChain, categoryPath } from "../utils/transform";
5
+ import type { NuvemshopCategory, NuvemshopList } from "../utils/types";
6
+
7
+ /**
8
+ * @title Nuvemshop - Categories
9
+ * @description Category tree for menus, with Nuvemshop-style nested URLs.
10
+ */
11
+ export default async function categories(
12
+ props: PageProps,
13
+ req?: Request,
14
+ ): Promise<SiteNavigationElement[]> {
15
+ const all =
16
+ (await nuvemshopGet<NuvemshopList<NuvemshopCategory>>("/categories", { per_page: 200 }))
17
+ ?.data ?? [];
18
+ const origin = originOf(props, req);
19
+ const node = (c: NuvemshopCategory): SiteNavigationElement => ({
20
+ "@type": "SiteNavigationElement",
21
+ name: c.name,
22
+ identifier: String(c.id),
23
+ url: `${origin}${categoryPath(categoryChain(c, all))}`,
24
+ children: all.filter((child) => child.parent === c.id).map(node),
25
+ });
26
+ return all.filter((c) => !c.parent).map(node);
27
+ }
@@ -0,0 +1,30 @@
1
+ import type { ProductDetailsPage } from "@decocms/apps-commerce/types";
2
+ import { getNuvemshopConfig, nuvemshopGet } from "../client";
3
+ import { originOf, type PageProps, pageUrlOf } from "../utils/request";
4
+ import { toProductPage } from "../utils/transform";
5
+ import type { NuvemshopProduct } from "../utils/types";
6
+
7
+ export interface Props extends PageProps {
8
+ /** @description Product handle or id. Defaults to the `/produtos/<handle>` URL segment (trailing slash optional). */
9
+ slug?: string;
10
+ }
11
+
12
+ /**
13
+ * @title Nuvemshop - Product Details Page
14
+ * @description Product by handle/id; `?variant=<id>` selects the variant.
15
+ */
16
+ export default async function productDetailsPage(
17
+ props: Props,
18
+ req?: Request,
19
+ ): Promise<ProductDetailsPage | null> {
20
+ const url = pageUrlOf(props, req);
21
+ const slug = props.slug ?? url.pathname.match(/\/produtos\/([^/]+)/)?.[1];
22
+ if (!slug) return null;
23
+ const product = await nuvemshopGet<NuvemshopProduct>(`/products/${encodeURIComponent(slug)}`);
24
+ if (!product?.variants?.length) return null;
25
+ return toProductPage(product, {
26
+ origin: originOf(props, req),
27
+ currency: getNuvemshopConfig().currency,
28
+ variantId: url.searchParams.get("variant") ?? url.searchParams.get("skuId"),
29
+ });
30
+ }
@@ -0,0 +1,49 @@
1
+ import type { Product } from "@decocms/apps-commerce/types";
2
+ import { getNuvemshopConfig, nuvemshopGet } from "../client";
3
+ import { originOf, type PageProps } from "../utils/request";
4
+ import { pickVariant, toProduct } from "../utils/transform";
5
+ import type { NuvemshopList, NuvemshopProduct, NuvemshopSort } from "../utils/types";
6
+
7
+ export interface Props extends PageProps {
8
+ /** @description Product ids (max 30). Takes precedence over the other selectors. */
9
+ ids?: number[];
10
+ /** @description Product handles (max 30). */
11
+ handles?: string[];
12
+ /** @description Category id (includes subcategories). */
13
+ categoryId?: number;
14
+ /** @description Search term. */
15
+ query?: string;
16
+ /** @description Ignored for ids/handles (kept in request order) and search. */
17
+ sort?: NuvemshopSort;
18
+ /** @default 12 */
19
+ count?: number;
20
+ }
21
+
22
+ /**
23
+ * @title Nuvemshop - Product List
24
+ * @description Shelf by ids, handles, category or search term.
25
+ */
26
+ export default async function productList(props: Props, req?: Request): Promise<Product[]> {
27
+ const count = props.count ?? 12;
28
+ const byKeys = props.ids?.length
29
+ ? { ids: props.ids.join(",") }
30
+ : props.handles?.length
31
+ ? { handles: props.handles.join(",") }
32
+ : null;
33
+ const list =
34
+ props.query && !byKeys
35
+ ? await nuvemshopGet<NuvemshopList<NuvemshopProduct>>("/search/products", {
36
+ q: props.query,
37
+ per_page: count,
38
+ })
39
+ : await nuvemshopGet<NuvemshopList<NuvemshopProduct>>("/products", {
40
+ ...byKeys,
41
+ // The API rejects sort_by combined with ids/handles.
42
+ ...(!byKeys && { category_id: props.categoryId, sort_by: props.sort }),
43
+ per_page: count,
44
+ });
45
+ const opts = { origin: originOf(props, req), currency: getNuvemshopConfig().currency };
46
+ return (list?.data ?? [])
47
+ .filter((p) => p.variants?.length)
48
+ .map((p) => toProduct(p, pickVariant(p), opts));
49
+ }
@@ -0,0 +1,94 @@
1
+ import type { ProductListingPage } from "@decocms/apps-commerce/types";
2
+ import { getNuvemshopConfig, nuvemshopGet } from "../client";
3
+ import { applyListing, LISTING_WINDOW, SORT_PARAM } from "../utils/listing";
4
+ import { originOf, type PageProps, pageUrlOf } from "../utils/request";
5
+ import { categoryChain, categoryPath, toBreadcrumbList } from "../utils/transform";
6
+ import type { NuvemshopCategory, NuvemshopList, NuvemshopProduct } from "../utils/types";
7
+
8
+ export interface Props extends PageProps {
9
+ /** @description Search term. Defaults to the `q` URL param. */
10
+ query?: string;
11
+ /** @description Pin a category. Defaults to the last segment of the URL path. */
12
+ categoryId?: number;
13
+ /** @description Products per page. @default 24 */
14
+ count?: number;
15
+ }
16
+
17
+ /**
18
+ * @title Nuvemshop - Product Listing Page
19
+ * @description Category or search page with in-memory facets (Cor/Tamanho/price), sort and pagination.
20
+ */
21
+ export default async function productListingPage(
22
+ props: Props,
23
+ req?: Request,
24
+ ): Promise<ProductListingPage | null> {
25
+ const url = pageUrlOf(props, req);
26
+ const origin = originOf(props, req);
27
+ const term = (props.query ?? url.searchParams.get("q") ?? "").trim();
28
+ const { currency, defaultSort = "created-descending" } = getNuvemshopConfig();
29
+ const opts = { origin, currency, count: props.count ?? 24 };
30
+
31
+ if (term) {
32
+ const found = await nuvemshopGet<NuvemshopList<NuvemshopProduct>>("/search/products", {
33
+ q: term,
34
+ per_page: LISTING_WINDOW,
35
+ });
36
+ const listing = applyListing(found?.data ?? [], url, opts);
37
+ return {
38
+ "@type": "ProductListingPage",
39
+ ...listing,
40
+ ...withPageType(listing.pageInfo, "Search"),
41
+ // /search/products takes no sort_by, so best-selling can't be honoured.
42
+ // Search keeps the API's relevance order by default; it can't do best-selling (no sort_by).
43
+ sortOptions: [
44
+ { value: "relevance", label: "Relevância" },
45
+ ...listing.sortOptions.filter((o) => o.value !== "best-selling"),
46
+ ],
47
+ breadcrumb: { "@type": "BreadcrumbList", itemListElement: [], numberOfItems: 0 },
48
+ seo: {
49
+ title: term,
50
+ description: "",
51
+ canonical: `${origin}${url.pathname}?q=${encodeURIComponent(term)}`,
52
+ noIndexing: true,
53
+ },
54
+ };
55
+ }
56
+
57
+ const handle = (props.__pagePath ?? url.pathname).split("/").filter(Boolean).at(-1);
58
+ const key = props.categoryId ?? handle;
59
+ if (!key) return null;
60
+ const [category, all] = await Promise.all([
61
+ nuvemshopGet<NuvemshopCategory>(`/categories/${encodeURIComponent(key)}`),
62
+ nuvemshopGet<NuvemshopList<NuvemshopCategory>>("/categories", { per_page: 200 }),
63
+ ]);
64
+ if (!category) return null;
65
+ const categories = all?.data ?? [category];
66
+ const leaf = categories.find((c) => c.id === category.id) ?? category;
67
+
68
+ const sort = url.searchParams.get(SORT_PARAM);
69
+ const list = await nuvemshopGet<NuvemshopList<NuvemshopProduct>>("/products", {
70
+ category_id: category.id,
71
+ per_page: LISTING_WINDOW,
72
+ sort_by: sort === "best-selling" ? sort : undefined,
73
+ });
74
+ const listing = applyListing(list?.data ?? [], url, { ...opts, defaultSort });
75
+
76
+ return {
77
+ "@type": "ProductListingPage",
78
+ ...listing,
79
+ ...withPageType(listing.pageInfo, leaf.parent ? "SubCategory" : "Category"),
80
+ breadcrumb: toBreadcrumbList(leaf, categories, origin),
81
+ seo: {
82
+ title: leaf.seo_title || leaf.name,
83
+ description: leaf.seo_description || leaf.description || "",
84
+ canonical: `${origin}${categoryPath(categoryChain(leaf, categories))}`,
85
+ },
86
+ };
87
+ }
88
+
89
+ const withPageType = (
90
+ pageInfo: ProductListingPage["pageInfo"],
91
+ type: "Category" | "SubCategory" | "Search",
92
+ ) => ({
93
+ pageInfo: { ...pageInfo, pageTypes: [type] },
94
+ });
@@ -0,0 +1,41 @@
1
+ import type { Product } from "@decocms/apps-commerce/types";
2
+ import { getNuvemshopConfig, nuvemshopGet } from "../client";
3
+ import { originOf, type PageProps, pageUrlOf } from "../utils/request";
4
+ import { isAvailable, pickVariant, toProduct } from "../utils/transform";
5
+ import type { NuvemshopList, NuvemshopProduct } from "../utils/types";
6
+
7
+ export interface Props extends PageProps {
8
+ /** @description Product handle or id. Defaults to the `/produtos/<handle>` URL segment (trailing slash optional). */
9
+ slug?: string;
10
+ /** @default 8 */
11
+ count?: number;
12
+ }
13
+
14
+ const CANDIDATES = 50;
15
+
16
+ /**
17
+ * @title Nuvemshop - Related Products
18
+ * @description Same rule as the Nuvemshop theme's fallback: products from the product's
19
+ * category, without itself, in-stock first, refilled with out-of-stock ones.
20
+ * (The theme prefers ids from a related-products app metafield, which the API doesn't expose.)
21
+ */
22
+ export default async function relatedProducts(props: Props, req?: Request): Promise<Product[]> {
23
+ const count = props.count ?? 8;
24
+ const slug = props.slug ?? pageUrlOf(props, req).pathname.match(/\/produtos\/([^/]+)/)?.[1];
25
+ if (!slug) return [];
26
+ const product = await nuvemshopGet<NuvemshopProduct>(`/products/${encodeURIComponent(slug)}`);
27
+ const category = product?.categories?.[0];
28
+ if (!product || !category) return [];
29
+
30
+ const list = await nuvemshopGet<NuvemshopList<NuvemshopProduct>>("/products", {
31
+ category_id: category.id,
32
+ per_page: CANDIDATES,
33
+ });
34
+ const others = (list?.data ?? []).filter((p) => p.id !== product.id && p.variants?.length);
35
+ const inStock = (p: NuvemshopProduct) => p.variants!.some(isAvailable);
36
+ // ponytail: no shuffle (the theme shuffles) — keeps the result cacheable.
37
+ const ordered = [...others.filter(inStock), ...others.filter((p) => !inStock(p))].slice(0, count);
38
+
39
+ const opts = { origin: originOf(props, req), currency: getNuvemshopConfig().currency };
40
+ return ordered.map((p) => toProduct(p, pickVariant(p), opts));
41
+ }
@@ -0,0 +1,29 @@
1
+ import { nuvemshopPost } from "../client";
2
+ import type { NuvemshopShippingOption, NuvemshopShippingOptions } from "../utils/types";
3
+
4
+ export interface Props {
5
+ zipCode: string;
6
+ variantId: number;
7
+ /** @default 1 */
8
+ quantity?: number;
9
+ }
10
+
11
+ export type ShippingOption = Omit<NuvemshopShippingOption, "price"> & { price: number };
12
+
13
+ /**
14
+ * @title Nuvemshop - Shipping Options
15
+ * @description Shipping quote for one variant + CEP (the API has no whole-cart quote yet).
16
+ */
17
+ export default async function shippingOptions(
18
+ props: Props,
19
+ req?: Request,
20
+ ): Promise<ShippingOption[]> {
21
+ const res = await nuvemshopPost<NuvemshopShippingOptions>(
22
+ "/shipping-options",
23
+ { zip_code: props.zipCode, variant_id: props.variantId, quantity: props.quantity ?? 1 },
24
+ { buyerIp: req?.headers.get("cf-connecting-ip") },
25
+ );
26
+ return res.shipping_options.map((o) => ({ ...o, price: Number(o.price) }));
27
+ }
28
+
29
+ export const cache = "no-store";
@@ -0,0 +1,24 @@
1
+ import type { Suggestion } from "@decocms/apps-commerce/types";
2
+ import type { PageProps } from "../utils/request";
3
+ import productList from "./productList";
4
+
5
+ export interface Props extends PageProps {
6
+ query?: string;
7
+ /** @default 6 (same as the theme's search suggestions) */
8
+ count?: number;
9
+ }
10
+
11
+ /**
12
+ * @title Nuvemshop - Search Suggestions
13
+ * @description Autocomplete: top products for the typed term.
14
+ */
15
+ export default async function suggestions(props: Props, req?: Request): Promise<Suggestion | null> {
16
+ const query = props.query?.trim();
17
+ if (!query) return null;
18
+ const products = await productList({ ...props, query, count: props.count ?? 6 }, req);
19
+ return {
20
+ searches: [{ term: query, href: `/search?q=${encodeURIComponent(query)}` }],
21
+ products,
22
+ hits: products.length,
23
+ };
24
+ }
@@ -0,0 +1,31 @@
1
+ // Hand-maintained (the generate-manifests script referenced by other apps doesn't exist).
2
+ // Keys are the `__resolveType` / `/deco/invoke/<key>` names.
3
+
4
+ import * as actions_createCheckout from "./actions/createCheckout";
5
+ import * as loaders_categories from "./loaders/categories";
6
+ import * as loaders_productDetailsPage from "./loaders/productDetailsPage";
7
+ import * as loaders_productList from "./loaders/productList";
8
+ import * as loaders_productListingPage from "./loaders/productListingPage";
9
+ import * as loaders_relatedProducts from "./loaders/relatedProducts";
10
+ import * as loaders_shippingOptions from "./loaders/shippingOptions";
11
+ import * as loaders_suggestions from "./loaders/suggestions";
12
+
13
+ const manifest = {
14
+ name: "nuvemshop",
15
+ loaders: {
16
+ "nuvemshop/loaders/categories": loaders_categories,
17
+ "nuvemshop/loaders/productDetailsPage": loaders_productDetailsPage,
18
+ "nuvemshop/loaders/productList": loaders_productList,
19
+ "nuvemshop/loaders/productListingPage": loaders_productListingPage,
20
+ "nuvemshop/loaders/relatedProducts": loaders_relatedProducts,
21
+ "nuvemshop/loaders/shippingOptions": loaders_shippingOptions,
22
+ "nuvemshop/loaders/suggestions": loaders_suggestions,
23
+ },
24
+ actions: {
25
+ "nuvemshop/actions/createCheckout": actions_createCheckout,
26
+ },
27
+ sections: {},
28
+ } as const;
29
+
30
+ export type Manifest = typeof manifest;
31
+ export default manifest;
package/src/mod.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Nuvemshop app module — standard autoconfig contract (`AppModContract`).
3
+ *
4
+ * Block `deco-nuvemshop`:
5
+ * ```json
6
+ * { "storeId": "8336778", "token": { "__resolveType": "website/loaders/secret.ts", ... }, "currency": "BRL" }
7
+ * ```
8
+ * The token is optional (tokenless access works) but recommended in
9
+ * production — see `client.ts` for the rate-limit trade-off.
10
+ */
11
+ import type { AppDefinition, ResolveSecretFn } from "@decocms/apps-commerce/app-types";
12
+ import { configureNuvemshop, type NuvemshopConfig } from "./client";
13
+ import manifest from "./manifest.gen";
14
+
15
+ export interface NuvemshopState {
16
+ config: NuvemshopConfig;
17
+ }
18
+
19
+ export async function configure(
20
+ block: Record<string, unknown>,
21
+ resolveSecret: ResolveSecretFn,
22
+ ): Promise<AppDefinition<NuvemshopState> | null> {
23
+ const storeId = block?.storeId == null ? "" : String(block.storeId);
24
+ if (!/^\d+$/.test(storeId)) return null;
25
+
26
+ const token =
27
+ (await resolveSecret(block.token, "NUVEMSHOP_STOREFRONT_TOKEN")) ??
28
+ (typeof block.token === "string" ? block.token : undefined);
29
+
30
+ const config: NuvemshopConfig = {
31
+ storeId,
32
+ token: token || undefined,
33
+ apiVersion: block.apiVersion as string | undefined,
34
+ currency: block.currency as string | undefined,
35
+ defaultSort: block.defaultSort as string | undefined,
36
+ };
37
+ configureNuvemshop(config);
38
+
39
+ return { name: "nuvemshop", manifest, state: { config } };
40
+ }
41
+
42
+ export const preview = undefined;
@@ -0,0 +1,9 @@
1
+ import type { AppRegistryEntry } from "@decocms/apps-commerce/registry";
2
+
3
+ export const NUVEMSHOP_REGISTRY_ENTRY: AppRegistryEntry = {
4
+ blockKey: "deco-nuvemshop",
5
+ module: () => import("./mod"),
6
+ displayName: "Nuvemshop",
7
+ category: "commerce",
8
+ description: "Nuvemshop (Tiendanube) Storefront API commerce integration",
9
+ };