@decocms/apps 0.20.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.
- package/.github/workflows/release.yml +34 -0
- package/.releaserc.json +25 -0
- package/commerce/components/Image.tsx +209 -0
- package/commerce/components/JsonLd.tsx +285 -0
- package/commerce/sdk/analytics.ts +24 -0
- package/commerce/sdk/formatPrice.ts +23 -0
- package/commerce/sdk/url.ts +9 -0
- package/commerce/sdk/useOffer.ts +75 -0
- package/commerce/sdk/useVariantPossibilities.ts +43 -0
- package/commerce/types/commerce.ts +1105 -0
- package/commerce/utils/canonical.ts +11 -0
- package/commerce/utils/constants.ts +9 -0
- package/commerce/utils/filters.ts +10 -0
- package/commerce/utils/productToAnalyticsItem.ts +67 -0
- package/commerce/utils/stateByZip.ts +50 -0
- package/knip.json +19 -0
- package/package.json +77 -0
- package/shopify/actions/cart/addItems.ts +37 -0
- package/shopify/actions/cart/updateCoupons.ts +32 -0
- package/shopify/actions/cart/updateItems.ts +32 -0
- package/shopify/actions/user/signIn.ts +45 -0
- package/shopify/actions/user/signUp.ts +36 -0
- package/shopify/client.ts +58 -0
- package/shopify/index.ts +32 -0
- package/shopify/init.ts +40 -0
- package/shopify/loaders/ProductDetailsPage.ts +35 -0
- package/shopify/loaders/ProductList.ts +101 -0
- package/shopify/loaders/ProductListingPage.ts +180 -0
- package/shopify/loaders/RelatedProducts.ts +45 -0
- package/shopify/loaders/cart.ts +73 -0
- package/shopify/loaders/shop.ts +40 -0
- package/shopify/loaders/user.ts +44 -0
- package/shopify/utils/admin/admin.ts +57 -0
- package/shopify/utils/admin/queries.ts +29 -0
- package/shopify/utils/cart.ts +28 -0
- package/shopify/utils/cookies.ts +85 -0
- package/shopify/utils/enums.ts +438 -0
- package/shopify/utils/graphql.ts +69 -0
- package/shopify/utils/storefront/queries.ts +530 -0
- package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
- package/shopify/utils/transform.ts +436 -0
- package/shopify/utils/types.ts +191 -0
- package/shopify/utils/user.ts +23 -0
- package/shopify/utils/utils.ts +164 -0
- package/tsconfig.json +11 -0
- package/vtex/README.md +6 -0
- package/vtex/actions/address.ts +211 -0
- package/vtex/actions/auth.ts +337 -0
- package/vtex/actions/checkout.ts +497 -0
- package/vtex/actions/index.ts +11 -0
- package/vtex/actions/masterData.ts +170 -0
- package/vtex/actions/misc.ts +196 -0
- package/vtex/actions/newsletter.ts +108 -0
- package/vtex/actions/orders.ts +37 -0
- package/vtex/actions/profile.ts +119 -0
- package/vtex/actions/session.ts +87 -0
- package/vtex/actions/trigger.ts +43 -0
- package/vtex/actions/wishlist.ts +116 -0
- package/vtex/client.ts +423 -0
- package/vtex/hooks/index.ts +4 -0
- package/vtex/hooks/useAutocomplete.ts +89 -0
- package/vtex/hooks/useCart.ts +219 -0
- package/vtex/hooks/useUser.ts +78 -0
- package/vtex/hooks/useWishlist.ts +119 -0
- package/vtex/index.ts +14 -0
- package/vtex/inline-loaders/productDetailsPage.ts +75 -0
- package/vtex/inline-loaders/productList.ts +163 -0
- package/vtex/inline-loaders/productListingPage.ts +447 -0
- package/vtex/inline-loaders/relatedProducts.ts +83 -0
- package/vtex/inline-loaders/suggestions.ts +49 -0
- package/vtex/inline-loaders/workflowProducts.ts +68 -0
- package/vtex/invoke.ts +202 -0
- package/vtex/loaders/address.ts +120 -0
- package/vtex/loaders/brands.ts +51 -0
- package/vtex/loaders/cart.ts +49 -0
- package/vtex/loaders/catalog.ts +165 -0
- package/vtex/loaders/collections.ts +57 -0
- package/vtex/loaders/index.ts +19 -0
- package/vtex/loaders/legacy.ts +671 -0
- package/vtex/loaders/logistics.ts +115 -0
- package/vtex/loaders/navbar.ts +29 -0
- package/vtex/loaders/orders.ts +103 -0
- package/vtex/loaders/pageType.ts +62 -0
- package/vtex/loaders/payment.ts +107 -0
- package/vtex/loaders/profile.ts +138 -0
- package/vtex/loaders/promotion.ts +33 -0
- package/vtex/loaders/search.ts +127 -0
- package/vtex/loaders/session.ts +91 -0
- package/vtex/loaders/user.ts +89 -0
- package/vtex/loaders/wishlist.ts +89 -0
- package/vtex/loaders/wishlistProducts.ts +81 -0
- package/vtex/loaders/workflow.ts +323 -0
- package/vtex/logo.png +0 -0
- package/vtex/middleware.ts +229 -0
- package/vtex/types.ts +248 -0
- package/vtex/utils/batch.ts +21 -0
- package/vtex/utils/cookies.ts +76 -0
- package/vtex/utils/enrichment.ts +540 -0
- package/vtex/utils/fetchCache.ts +150 -0
- package/vtex/utils/index.ts +17 -0
- package/vtex/utils/intelligentSearch.ts +84 -0
- package/vtex/utils/legacy.ts +155 -0
- package/vtex/utils/pickAndOmit.ts +30 -0
- package/vtex/utils/proxy.ts +196 -0
- package/vtex/utils/resourceRange.ts +10 -0
- package/vtex/utils/segment.ts +163 -0
- package/vtex/utils/similars.ts +38 -0
- package/vtex/utils/sitemap.ts +133 -0
- package/vtex/utils/slugCache.ts +32 -0
- package/vtex/utils/slugify.ts +13 -0
- package/vtex/utils/transform.ts +1331 -0
- package/vtex/utils/types.ts +1884 -0
- package/vtex/utils/vtexId.ts +103 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BreadcrumbList } from "../types/commerce";
|
|
2
|
+
|
|
3
|
+
export const canonicalFromBreadcrumblist = (
|
|
4
|
+
b?: BreadcrumbList,
|
|
5
|
+
) => {
|
|
6
|
+
const items = b?.itemListElement ?? [];
|
|
7
|
+
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
8
|
+
|
|
9
|
+
return items.reduce((acc, curr) => acc.position < curr.position ? curr : acc)
|
|
10
|
+
.item;
|
|
11
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ImageObject } from "../types/commerce";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_IMAGE: ImageObject = {
|
|
4
|
+
"@type": "ImageObject",
|
|
5
|
+
encodingFormat: "image",
|
|
6
|
+
alternateName: "Default Image Placeholder",
|
|
7
|
+
url:
|
|
8
|
+
"https://ozksgdmyrqcxcwhnbepg.supabase.co/storage/v1/object/public/assets/1818/ff6bb37e-0eab-40e1-a454-86856efc278e",
|
|
9
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const parseRange = (price: string) => {
|
|
2
|
+
const splitted = price.split(":");
|
|
3
|
+
|
|
4
|
+
const from = Number(splitted?.[0]);
|
|
5
|
+
const to = Number(splitted?.[1]);
|
|
6
|
+
|
|
7
|
+
return Number.isNaN(from) || Number.isNaN(to) ? null : { from, to };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const formatRange = (from: number, to: number) => `${from}:${to}`;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AnalyticsItem, BreadcrumbList, Product } from "../types/commerce";
|
|
2
|
+
|
|
3
|
+
export const mapCategoriesToAnalyticsCategories = (
|
|
4
|
+
categories: string[],
|
|
5
|
+
): Record<`item_category${number | ""}`, string> => {
|
|
6
|
+
return categories.slice(0, 5).reduce(
|
|
7
|
+
(result, category, index) => {
|
|
8
|
+
result[`item_category${index === 0 ? "" : index + 1}`] = category;
|
|
9
|
+
return result;
|
|
10
|
+
},
|
|
11
|
+
{} as Record<`item_category${number | ""}`, string>,
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const mapProductCategoryToAnalyticsCategories = (category: string) => {
|
|
16
|
+
return category.split(">").reduce(
|
|
17
|
+
(result, category, index) => {
|
|
18
|
+
result[`item_category${index === 0 ? "" : index + 1}`] = category.trim();
|
|
19
|
+
return result;
|
|
20
|
+
},
|
|
21
|
+
{} as Record<`item_category${number | ""}`, string>,
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const mapProductToAnalyticsItem = (
|
|
26
|
+
{
|
|
27
|
+
product,
|
|
28
|
+
breadcrumbList,
|
|
29
|
+
price,
|
|
30
|
+
listPrice,
|
|
31
|
+
index = 0,
|
|
32
|
+
quantity = 1,
|
|
33
|
+
coupon = "",
|
|
34
|
+
}: {
|
|
35
|
+
product: Product;
|
|
36
|
+
breadcrumbList?: BreadcrumbList;
|
|
37
|
+
price?: number;
|
|
38
|
+
listPrice?: number;
|
|
39
|
+
index?: number;
|
|
40
|
+
quantity?: number;
|
|
41
|
+
coupon?: string;
|
|
42
|
+
},
|
|
43
|
+
): AnalyticsItem => {
|
|
44
|
+
const { name, productID, inProductGroupWithID, isVariantOf, url } = product;
|
|
45
|
+
const categories = breadcrumbList?.itemListElement
|
|
46
|
+
? mapCategoriesToAnalyticsCategories(
|
|
47
|
+
breadcrumbList?.itemListElement.map(({ name: _name }) => _name ?? "")
|
|
48
|
+
.filter(Boolean) ??
|
|
49
|
+
[],
|
|
50
|
+
)
|
|
51
|
+
: mapProductCategoryToAnalyticsCategories(product.category ?? "");
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
item_id: productID,
|
|
55
|
+
item_group_id: inProductGroupWithID,
|
|
56
|
+
quantity,
|
|
57
|
+
coupon,
|
|
58
|
+
price,
|
|
59
|
+
index,
|
|
60
|
+
discount: Number((price && listPrice ? listPrice - price : 0).toFixed(2)),
|
|
61
|
+
item_name: isVariantOf?.name ?? name ?? "",
|
|
62
|
+
item_variant: name,
|
|
63
|
+
item_brand: product.brand?.name ?? "",
|
|
64
|
+
item_url: url,
|
|
65
|
+
...categories,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const getStateFromZip = (cep: string) => {
|
|
2
|
+
// Remove non-numeric characters
|
|
3
|
+
cep = cep.replace(/\D/g, "");
|
|
4
|
+
|
|
5
|
+
// zip range by: https://buscacepinter.correios.com.br/app/faixa_cep_uf_localidade/index.php
|
|
6
|
+
const zipRange = [
|
|
7
|
+
{ state: "AC", startRange: 69900000, endRange: 69999999 },
|
|
8
|
+
{ state: "AL", startRange: 57000000, endRange: 57999999 },
|
|
9
|
+
{ state: "AM", startRange: 69000000, endRange: 69299999 },
|
|
10
|
+
{ state: "AM", startRange: 69400000, endRange: 69899999 },
|
|
11
|
+
{ state: "AP", startRange: 68900000, endRange: 68999999 },
|
|
12
|
+
{ state: "BA", startRange: 40000000, endRange: 48999999 },
|
|
13
|
+
{ state: "CE", startRange: 60000000, endRange: 63999999 },
|
|
14
|
+
{ state: "DF", startRange: 70000000, endRange: 72799999 },
|
|
15
|
+
{ state: "DF", startRange: 73000000, endRange: 73699999 },
|
|
16
|
+
{ state: "ES", startRange: 29000000, endRange: 29999999 },
|
|
17
|
+
{ state: "GO", startRange: 72800000, endRange: 72999999 },
|
|
18
|
+
{ state: "GO", startRange: 73700000, endRange: 76799999 },
|
|
19
|
+
{ state: "MA", startRange: 65000000, endRange: 65999999 },
|
|
20
|
+
{ state: "MG", startRange: 30000000, endRange: 39999999 },
|
|
21
|
+
{ state: "MS", startRange: 79000000, endRange: 79999999 },
|
|
22
|
+
{ state: "MT", startRange: 78000000, endRange: 78899999 },
|
|
23
|
+
{ state: "PA", startRange: 66000000, endRange: 68899999 },
|
|
24
|
+
{ state: "PB", startRange: 58000000, endRange: 58999999 },
|
|
25
|
+
{ state: "PE", startRange: 50000000, endRange: 56999999 },
|
|
26
|
+
{ state: "PI", startRange: 64000000, endRange: 64999999 },
|
|
27
|
+
{ state: "PR", startRange: 80000000, endRange: 87999999 },
|
|
28
|
+
{ state: "RJ", startRange: 20000000, endRange: 28999999 },
|
|
29
|
+
{ state: "RN", startRange: 59000000, endRange: 59999999 },
|
|
30
|
+
{ state: "RO", startRange: 76800000, endRange: 76999999 },
|
|
31
|
+
{ state: "RR", startRange: 69300000, endRange: 69399999 },
|
|
32
|
+
{ state: "RS", startRange: 90000000, endRange: 99999999 },
|
|
33
|
+
{ state: "SC", startRange: 88000000, endRange: 89999999 },
|
|
34
|
+
{ state: "SE", startRange: 49000000, endRange: 49999999 },
|
|
35
|
+
{ state: "SP", startRange: 1000000, endRange: 19999999 },
|
|
36
|
+
{ state: "TO", startRange: 77000000, endRange: 77999999 },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const zipCode = parseInt(cep);
|
|
40
|
+
|
|
41
|
+
for (const range of zipRange) {
|
|
42
|
+
if (zipCode >= range.startRange && zipCode <= range.endRange) {
|
|
43
|
+
return range.state;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return "";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default getStateFromZip;
|
package/knip.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/knip@latest/schema.json",
|
|
3
|
+
"entry": [
|
|
4
|
+
"vtex/*.ts",
|
|
5
|
+
"vtex/inline-loaders/*.ts",
|
|
6
|
+
"commerce/components/*.tsx",
|
|
7
|
+
"commerce/sdk/*.ts"
|
|
8
|
+
],
|
|
9
|
+
"project": [
|
|
10
|
+
"**/*.ts",
|
|
11
|
+
"**/*.tsx"
|
|
12
|
+
],
|
|
13
|
+
"ignoreBinaries": [
|
|
14
|
+
"semantic-release"
|
|
15
|
+
],
|
|
16
|
+
"ignoreDependencies": [
|
|
17
|
+
"@decocms/start"
|
|
18
|
+
]
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decocms/apps",
|
|
3
|
+
"version": "0.20.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./commerce/types": "./commerce/types/commerce.ts",
|
|
8
|
+
"./commerce/components/JsonLd": "./commerce/components/JsonLd.tsx",
|
|
9
|
+
"./commerce/components/Image": "./commerce/components/Image.tsx",
|
|
10
|
+
"./commerce/utils/*": "./commerce/utils/*.ts",
|
|
11
|
+
"./commerce/sdk/*": "./commerce/sdk/*.ts",
|
|
12
|
+
"./shopify": "./shopify/index.ts",
|
|
13
|
+
"./shopify/client": "./shopify/client.ts",
|
|
14
|
+
"./shopify/loaders/*": "./shopify/loaders/*.ts",
|
|
15
|
+
"./shopify/actions/*": "./shopify/actions/*.ts",
|
|
16
|
+
"./shopify/actions/cart/*": "./shopify/actions/cart/*.ts",
|
|
17
|
+
"./shopify/actions/user/*": "./shopify/actions/user/*.ts",
|
|
18
|
+
"./shopify/utils/*": "./shopify/utils/*.ts",
|
|
19
|
+
"./vtex": "./vtex/index.ts",
|
|
20
|
+
"./vtex/client": "./vtex/client.ts",
|
|
21
|
+
"./vtex/types": "./vtex/types.ts",
|
|
22
|
+
"./vtex/actions": "./vtex/actions/index.ts",
|
|
23
|
+
"./vtex/actions/*": "./vtex/actions/*.ts",
|
|
24
|
+
"./vtex/loaders": "./vtex/loaders/index.ts",
|
|
25
|
+
"./vtex/loaders/*": "./vtex/loaders/*.ts",
|
|
26
|
+
"./vtex/utils": "./vtex/utils/index.ts",
|
|
27
|
+
"./vtex/utils/*": "./vtex/utils/*.ts",
|
|
28
|
+
"./vtex/inline-loaders/*": "./vtex/inline-loaders/*.ts",
|
|
29
|
+
"./vtex/inline-loaders/productDetailsPage": "./vtex/inline-loaders/productDetailsPage.ts",
|
|
30
|
+
"./vtex/inline-loaders/productListingPage": "./vtex/inline-loaders/productListingPage.ts",
|
|
31
|
+
"./vtex/inline-loaders/productList": "./vtex/inline-loaders/productList.ts",
|
|
32
|
+
"./vtex/inline-loaders/relatedProducts": "./vtex/inline-loaders/relatedProducts.ts",
|
|
33
|
+
"./vtex/inline-loaders/suggestions": "./vtex/inline-loaders/suggestions.ts",
|
|
34
|
+
"./vtex/hooks": "./vtex/hooks/index.ts",
|
|
35
|
+
"./vtex/hooks/*": "./vtex/hooks/*.ts",
|
|
36
|
+
"./vtex/inline-loaders/workflowProducts": "./vtex/inline-loaders/workflowProducts.ts",
|
|
37
|
+
"./vtex/middleware": "./vtex/middleware.ts",
|
|
38
|
+
"./vtex/invoke": "./vtex/invoke.ts"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint:unused": "knip",
|
|
43
|
+
"lint:unused:fix": "knip --fix",
|
|
44
|
+
"check": "tsc --noEmit && knip"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"deco",
|
|
48
|
+
"shopify",
|
|
49
|
+
"vtex",
|
|
50
|
+
"commerce"
|
|
51
|
+
],
|
|
52
|
+
"author": "deco.cx",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/decocms/apps-start.git"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org",
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@decocms/start": "~0.19.0",
|
|
64
|
+
"@tanstack/react-query": ">=5",
|
|
65
|
+
"react": ">=18",
|
|
66
|
+
"react-dom": ">=18"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@decocms/start": "file:../deco-start",
|
|
70
|
+
"@semantic-release/git": "^10.0.1",
|
|
71
|
+
"@tanstack/react-query": "^5.90.21",
|
|
72
|
+
"@types/react": "^19.0.0",
|
|
73
|
+
"knip": "^5.86.0",
|
|
74
|
+
"react": "^19.0.0",
|
|
75
|
+
"typescript": "^5.9.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getShopifyClient } from "../../client";
|
|
2
|
+
import { getCartCookie, setCartCookie } from "../../utils/cart";
|
|
3
|
+
import { AddItemToCart } from "../../utils/storefront/queries";
|
|
4
|
+
import type { ShopifyCart } from "../../loaders/cart";
|
|
5
|
+
|
|
6
|
+
export interface AddItemProps {
|
|
7
|
+
lines: {
|
|
8
|
+
merchandiseId: string;
|
|
9
|
+
attributes?: Array<{ key: string; value: string }>;
|
|
10
|
+
quantity?: number;
|
|
11
|
+
sellingPlanId?: string;
|
|
12
|
+
};
|
|
13
|
+
requestHeaders: Headers;
|
|
14
|
+
responseHeaders?: Headers;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function addItems(
|
|
18
|
+
{ lines, requestHeaders, responseHeaders }: AddItemProps,
|
|
19
|
+
): Promise<ShopifyCart | null> {
|
|
20
|
+
const client = getShopifyClient();
|
|
21
|
+
const cartId = getCartCookie(requestHeaders);
|
|
22
|
+
|
|
23
|
+
if (!cartId) throw new Error("Missing cart cookie");
|
|
24
|
+
|
|
25
|
+
const data = await client.query<{
|
|
26
|
+
payload?: { cart?: ShopifyCart };
|
|
27
|
+
}>(
|
|
28
|
+
AddItemToCart,
|
|
29
|
+
{ cartId, lines },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (responseHeaders) {
|
|
33
|
+
setCartCookie(responseHeaders, cartId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return data.payload?.cart ?? null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getShopifyClient } from "../../client";
|
|
2
|
+
import { getCartCookie, setCartCookie } from "../../utils/cart";
|
|
3
|
+
import { AddCoupon } from "../../utils/storefront/queries";
|
|
4
|
+
import type { ShopifyCart } from "../../loaders/cart";
|
|
5
|
+
|
|
6
|
+
export interface UpdateCouponsProps {
|
|
7
|
+
discountCodes: string[];
|
|
8
|
+
requestHeaders: Headers;
|
|
9
|
+
responseHeaders?: Headers;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function updateCoupons(
|
|
13
|
+
{ discountCodes, requestHeaders, responseHeaders }: UpdateCouponsProps,
|
|
14
|
+
): Promise<ShopifyCart | null> {
|
|
15
|
+
const client = getShopifyClient();
|
|
16
|
+
const cartId = getCartCookie(requestHeaders);
|
|
17
|
+
|
|
18
|
+
if (!cartId) throw new Error("Missing cart cookie");
|
|
19
|
+
|
|
20
|
+
const data = await client.query<{
|
|
21
|
+
payload?: { cart?: ShopifyCart };
|
|
22
|
+
}>(
|
|
23
|
+
AddCoupon,
|
|
24
|
+
{ cartId, discountCodes },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (responseHeaders) {
|
|
28
|
+
setCartCookie(responseHeaders, cartId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return data.payload?.cart ?? null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getShopifyClient } from "../../client";
|
|
2
|
+
import { getCartCookie, setCartCookie } from "../../utils/cart";
|
|
3
|
+
import { UpdateItems } from "../../utils/storefront/queries";
|
|
4
|
+
import type { ShopifyCart } from "../../loaders/cart";
|
|
5
|
+
|
|
6
|
+
export interface UpdateItemsProps {
|
|
7
|
+
lines: Array<{ id: string; quantity: number }>;
|
|
8
|
+
requestHeaders: Headers;
|
|
9
|
+
responseHeaders?: Headers;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function updateItems(
|
|
13
|
+
{ lines, requestHeaders, responseHeaders }: UpdateItemsProps,
|
|
14
|
+
): Promise<ShopifyCart | null> {
|
|
15
|
+
const client = getShopifyClient();
|
|
16
|
+
const cartId = getCartCookie(requestHeaders);
|
|
17
|
+
|
|
18
|
+
if (!cartId) throw new Error("Missing cart cookie");
|
|
19
|
+
|
|
20
|
+
const data = await client.query<{
|
|
21
|
+
payload?: { cart?: ShopifyCart };
|
|
22
|
+
}>(
|
|
23
|
+
UpdateItems,
|
|
24
|
+
{ cartId, lines },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (responseHeaders) {
|
|
28
|
+
setCartCookie(responseHeaders, cartId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return data.payload?.cart ?? null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getShopifyClient } from "../../client";
|
|
2
|
+
import { SignInWithEmailAndPassword } from "../../utils/storefront/queries";
|
|
3
|
+
import { getUserCookie, setUserCookie } from "../../utils/user";
|
|
4
|
+
|
|
5
|
+
export interface SignInProps {
|
|
6
|
+
email: string;
|
|
7
|
+
password: string;
|
|
8
|
+
requestHeaders: Headers;
|
|
9
|
+
responseHeaders?: Headers;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SignInResult {
|
|
13
|
+
customerAccessTokenCreate: {
|
|
14
|
+
customerAccessToken?: { accessToken: string; expiresAt: string } | null;
|
|
15
|
+
customerUserErrors?: Array<{ code?: string; message: string }>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function signIn(
|
|
20
|
+
props: SignInProps,
|
|
21
|
+
): Promise<SignInResult | null> {
|
|
22
|
+
const client = getShopifyClient();
|
|
23
|
+
const { email, password, requestHeaders, responseHeaders } = props;
|
|
24
|
+
|
|
25
|
+
const existingToken = getUserCookie(requestHeaders);
|
|
26
|
+
if (existingToken) return null;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const data = await client.query<SignInResult>(
|
|
30
|
+
SignInWithEmailAndPassword,
|
|
31
|
+
{ email, password },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (data.customerAccessTokenCreate.customerAccessToken && responseHeaders) {
|
|
35
|
+
setUserCookie(
|
|
36
|
+
responseHeaders,
|
|
37
|
+
data.customerAccessTokenCreate.customerAccessToken.accessToken,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return data;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getShopifyClient } from "../../client";
|
|
2
|
+
import { RegisterAccount } from "../../utils/storefront/queries";
|
|
3
|
+
|
|
4
|
+
export interface SignUpProps {
|
|
5
|
+
email: string;
|
|
6
|
+
password: string;
|
|
7
|
+
firstName?: string;
|
|
8
|
+
lastName?: string;
|
|
9
|
+
acceptsMarketing?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SignUpResult {
|
|
13
|
+
customerCreate: {
|
|
14
|
+
customer?: { id: string } | null;
|
|
15
|
+
customerUserErrors?: Array<{ code?: string; message: string }>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function signUp(
|
|
20
|
+
props: SignUpProps,
|
|
21
|
+
): Promise<SignUpResult> {
|
|
22
|
+
const client = getShopifyClient();
|
|
23
|
+
|
|
24
|
+
const data = await client.query<SignUpResult>(
|
|
25
|
+
RegisterAccount,
|
|
26
|
+
{
|
|
27
|
+
email: props.email,
|
|
28
|
+
password: props.password,
|
|
29
|
+
firstName: props.firstName,
|
|
30
|
+
lastName: props.lastName,
|
|
31
|
+
acceptsMarketing: props.acceptsMarketing,
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createGraphqlClient, type GraphQLClient } from "./utils/graphql";
|
|
2
|
+
|
|
3
|
+
export interface ShopifyConfig {
|
|
4
|
+
storeName: string;
|
|
5
|
+
storefrontAccessToken: string;
|
|
6
|
+
publicUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let _client: GraphQLClient | null = null;
|
|
10
|
+
let _config: ShopifyConfig | null = null;
|
|
11
|
+
let _fetch: typeof fetch | undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Override the fetch function used by the Shopify GraphQL client.
|
|
15
|
+
* Use this to plug in instrumented fetch for logging/tracing.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
|
|
20
|
+
* import { setShopifyFetch } from "@decocms/apps/shopify";
|
|
21
|
+
* setShopifyFetch(createInstrumentedFetch("shopify"));
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function setShopifyFetch(fetchFn: typeof fetch) {
|
|
25
|
+
_fetch = fetchFn;
|
|
26
|
+
if (_config) configureShopify(_config);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function configureShopify(config: ShopifyConfig) {
|
|
30
|
+
_config = config;
|
|
31
|
+
_client = createGraphqlClient(
|
|
32
|
+
`https://${config.storeName}.myshopify.com/api/2025-04/graphql.json`,
|
|
33
|
+
{
|
|
34
|
+
"X-Shopify-Storefront-Access-Token": config.storefrontAccessToken,
|
|
35
|
+
},
|
|
36
|
+
_fetch,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getShopifyClient(): GraphQLClient {
|
|
41
|
+
if (!_client || !_config) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Shopify not configured. Call configureShopify() first or check deco-shopify.json block."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return _client;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getShopifyConfig(): ShopifyConfig {
|
|
50
|
+
if (!_config) {
|
|
51
|
+
throw new Error("Shopify not configured.");
|
|
52
|
+
}
|
|
53
|
+
return _config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getBaseUrl(): string {
|
|
57
|
+
return _config?.publicUrl || "";
|
|
58
|
+
}
|
package/shopify/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Client & Config
|
|
2
|
+
export { configureShopify, getShopifyClient, getShopifyConfig, getBaseUrl, setShopifyFetch } from "./client";
|
|
3
|
+
export type { ShopifyConfig } from "./client";
|
|
4
|
+
export { initShopify, initShopifyFromBlocks } from "./init";
|
|
5
|
+
|
|
6
|
+
// Product Loaders
|
|
7
|
+
export { default as productListLoader } from "./loaders/ProductList";
|
|
8
|
+
export { default as productDetailsPageLoader } from "./loaders/ProductDetailsPage";
|
|
9
|
+
export { default as productListingPageLoader } from "./loaders/ProductListingPage";
|
|
10
|
+
export { default as relatedProductsLoader } from "./loaders/RelatedProducts";
|
|
11
|
+
|
|
12
|
+
// Cart
|
|
13
|
+
export { getCart, createCart } from "./loaders/cart";
|
|
14
|
+
export type { ShopifyCart, CartLine } from "./loaders/cart";
|
|
15
|
+
export { default as addItems } from "./actions/cart/addItems";
|
|
16
|
+
export { default as updateItems } from "./actions/cart/updateItems";
|
|
17
|
+
export { default as updateCoupons } from "./actions/cart/updateCoupons";
|
|
18
|
+
|
|
19
|
+
// Shop
|
|
20
|
+
export { default as shopLoader } from "./loaders/shop";
|
|
21
|
+
export type { Shop } from "./loaders/shop";
|
|
22
|
+
|
|
23
|
+
// User
|
|
24
|
+
export { default as userLoader } from "./loaders/user";
|
|
25
|
+
export type { ShopifyUser } from "./loaders/user";
|
|
26
|
+
export { default as signIn } from "./actions/user/signIn";
|
|
27
|
+
export { default as signUp } from "./actions/user/signUp";
|
|
28
|
+
|
|
29
|
+
// Cookie utils
|
|
30
|
+
export { getCookies, setCookie } from "./utils/cookies";
|
|
31
|
+
export { getCartCookie, setCartCookie } from "./utils/cart";
|
|
32
|
+
export { getUserCookie, setUserCookie } from "./utils/user";
|
package/shopify/init.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { configureShopify } from "./client";
|
|
2
|
+
|
|
3
|
+
let initialized = false;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize Shopify from raw block data.
|
|
7
|
+
* The site is responsible for reading the blocks and passing the config here.
|
|
8
|
+
*/
|
|
9
|
+
export function initShopify(config: {
|
|
10
|
+
storeName: string;
|
|
11
|
+
storefrontAccessToken: string;
|
|
12
|
+
}) {
|
|
13
|
+
if (initialized) return;
|
|
14
|
+
|
|
15
|
+
if (!config.storeName || !config.storefrontAccessToken) {
|
|
16
|
+
console.warn("[Shopify] Missing storeName or storefrontAccessToken.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(`[Shopify] Initializing: ${config.storeName}.myshopify.com`);
|
|
21
|
+
configureShopify(config);
|
|
22
|
+
initialized = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize Shopify from a blocks map (convenience wrapper).
|
|
27
|
+
* Looks for the "deco-shopify" block and extracts credentials.
|
|
28
|
+
*/
|
|
29
|
+
export function initShopifyFromBlocks(blocks: Record<string, any>) {
|
|
30
|
+
const shopifyBlock = blocks["deco-shopify"];
|
|
31
|
+
if (!shopifyBlock) {
|
|
32
|
+
console.warn("[Shopify] No deco-shopify block found.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
initShopify({
|
|
37
|
+
storeName: shopifyBlock.storeName,
|
|
38
|
+
storefrontAccessToken: shopifyBlock.storefrontAccessToken,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ProductDetailsPage } from "../../commerce/types/commerce";
|
|
2
|
+
import { getShopifyClient } from "../client";
|
|
3
|
+
import { GetProduct } from "../utils/storefront/queries";
|
|
4
|
+
import { toProductPage, type ProductShopify } from "../utils/transform";
|
|
5
|
+
import type { Metafield } from "../utils/types";
|
|
6
|
+
|
|
7
|
+
export interface Props {
|
|
8
|
+
slug: string;
|
|
9
|
+
metafields?: Metafield[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function productDetailsPageLoader(
|
|
13
|
+
props: Props,
|
|
14
|
+
url?: URL,
|
|
15
|
+
): Promise<ProductDetailsPage | null> {
|
|
16
|
+
const client = getShopifyClient();
|
|
17
|
+
const { slug, metafields = [] } = props;
|
|
18
|
+
|
|
19
|
+
const splitted = slug?.split("-") ?? [];
|
|
20
|
+
const maybeSkuId = Number(splitted[splitted.length - 1]);
|
|
21
|
+
const handle = splitted.slice(0, maybeSkuId ? -1 : undefined).join("-");
|
|
22
|
+
|
|
23
|
+
const data = await client.query<{ product?: ProductShopify }>(
|
|
24
|
+
GetProduct,
|
|
25
|
+
{ handle, identifiers: metafields },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!data?.product) return null;
|
|
29
|
+
|
|
30
|
+
return toProductPage(
|
|
31
|
+
data.product,
|
|
32
|
+
url ?? new URL("https://localhost"),
|
|
33
|
+
maybeSkuId || undefined,
|
|
34
|
+
);
|
|
35
|
+
}
|