@decocms/apps-shopify 8.0.0 → 8.1.0-next.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/package.json +7 -24
- package/src/client.ts +113 -46
- package/src/{utils/graphqlOperationName.ts → graphqlOperationName.ts} +22 -22
- package/src/index.ts +8 -41
- package/src/actions/cart/addItems.ts +0 -36
- package/src/actions/cart/updateCoupons.ts +0 -31
- package/src/actions/cart/updateItems.ts +0 -31
- package/src/actions/user/signIn.ts +0 -40
- package/src/actions/user/signUp.ts +0 -31
- package/src/init.ts +0 -39
- package/src/loaders/ProductDetailsPage.ts +0 -31
- package/src/loaders/ProductList.ts +0 -102
- package/src/loaders/ProductListingPage.ts +0 -172
- package/src/loaders/RelatedProducts.ts +0 -42
- package/src/loaders/cart.ts +0 -74
- package/src/loaders/shop.ts +0 -35
- package/src/loaders/user.ts +0 -39
- package/src/manifest.gen.ts +0 -39
- package/src/mod.ts +0 -67
- package/src/registry.ts +0 -9
- package/src/utils/__tests__/graphqlOperationName.test.ts +0 -80
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -84
- package/src/utils/__tests__/operationRouter.test.ts +0 -56
- package/src/utils/__tests__/transform.test.ts +0 -216
- package/src/utils/admin/admin.ts +0 -57
- package/src/utils/admin/queries.ts +0 -29
- package/src/utils/cart.ts +0 -32
- package/src/utils/cookies.ts +0 -82
- package/src/utils/enums.ts +0 -438
- package/src/utils/graphql.ts +0 -69
- package/src/utils/instrumentedFetch.ts +0 -55
- package/src/utils/operationRouter.ts +0 -70
- package/src/utils/storefront/queries.ts +0 -525
- package/src/utils/storefront/storefront.graphql.gen.ts +0 -129
- package/src/utils/transform.ts +0 -430
- package/src/utils/types.ts +0 -191
- package/src/utils/user.ts +0 -23
- package/src/utils/utils.ts +0 -159
- package/tsconfig.json +0 -7
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import type { Product } from "@decocms/apps-commerce/types";
|
|
2
|
-
import { getShopifyClient } from "../client";
|
|
3
|
-
import { ProductsByCollection, SearchProducts } from "../utils/storefront/queries";
|
|
4
|
-
import { type ProductShopify, toProduct } from "../utils/transform";
|
|
5
|
-
import type { Metafield } from "../utils/types";
|
|
6
|
-
import {
|
|
7
|
-
type CollectionSortKeys,
|
|
8
|
-
type SearchSortKeys,
|
|
9
|
-
searchSortShopify,
|
|
10
|
-
sortShopify,
|
|
11
|
-
} from "../utils/utils";
|
|
12
|
-
|
|
13
|
-
export interface QueryProps {
|
|
14
|
-
query: string;
|
|
15
|
-
count: number;
|
|
16
|
-
sort?: SearchSortKeys;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface CollectionProps {
|
|
20
|
-
collection: string;
|
|
21
|
-
count: number;
|
|
22
|
-
sort?: CollectionSortKeys;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface FilterProps {
|
|
26
|
-
tags?: string[];
|
|
27
|
-
productTypes?: string[];
|
|
28
|
-
productVendors?: string[];
|
|
29
|
-
priceMin?: number;
|
|
30
|
-
priceMax?: number;
|
|
31
|
-
variantOptions?: { name: string; value: string }[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type Props = {
|
|
35
|
-
props: QueryProps | CollectionProps;
|
|
36
|
-
filters?: FilterProps;
|
|
37
|
-
metafields?: Metafield[];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const isQueryList = (p: QueryProps | CollectionProps): p is QueryProps =>
|
|
41
|
-
"query" in p && typeof p.query === "string" && typeof p.count === "number";
|
|
42
|
-
|
|
43
|
-
export default async function productListLoader(
|
|
44
|
-
expandedProps: Props,
|
|
45
|
-
url?: URL,
|
|
46
|
-
): Promise<Product[] | null> {
|
|
47
|
-
const client = getShopifyClient();
|
|
48
|
-
|
|
49
|
-
const props = expandedProps.props ?? (expandedProps as unknown as Props["props"]);
|
|
50
|
-
|
|
51
|
-
const count = props.count ?? 12;
|
|
52
|
-
const metafields = expandedProps.metafields || [];
|
|
53
|
-
const sort = props.sort ?? "";
|
|
54
|
-
|
|
55
|
-
const filters: Record<string, unknown>[] = [];
|
|
56
|
-
for (const tag of expandedProps.filters?.tags ?? []) {
|
|
57
|
-
filters.push({ tag });
|
|
58
|
-
}
|
|
59
|
-
for (const productType of expandedProps.filters?.productTypes ?? []) {
|
|
60
|
-
filters.push({ productType });
|
|
61
|
-
}
|
|
62
|
-
for (const productVendor of expandedProps.filters?.productVendors ?? []) {
|
|
63
|
-
filters.push({ productVendor });
|
|
64
|
-
}
|
|
65
|
-
if (expandedProps.filters?.priceMin != null)
|
|
66
|
-
filters.push({ price: { min: expandedProps.filters.priceMin } });
|
|
67
|
-
if (expandedProps.filters?.priceMax != null)
|
|
68
|
-
filters.push({ price: { max: expandedProps.filters.priceMax } });
|
|
69
|
-
for (const variantOption of expandedProps.filters?.variantOptions ?? []) {
|
|
70
|
-
filters.push({ variantOption });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let shopifyProducts: { nodes: ProductShopify[] } | undefined;
|
|
74
|
-
|
|
75
|
-
if (isQueryList(props)) {
|
|
76
|
-
const data = await client.query<{ search: { nodes: ProductShopify[] } }>(SearchProducts, {
|
|
77
|
-
first: count,
|
|
78
|
-
query: props.query,
|
|
79
|
-
productFilters: filters,
|
|
80
|
-
identifiers: metafields,
|
|
81
|
-
...searchSortShopify[sort],
|
|
82
|
-
});
|
|
83
|
-
shopifyProducts = data.search;
|
|
84
|
-
} else {
|
|
85
|
-
const data = await client.query<{
|
|
86
|
-
collection?: { products: { nodes: ProductShopify[] } };
|
|
87
|
-
}>(ProductsByCollection, {
|
|
88
|
-
first: count,
|
|
89
|
-
handle: (props as CollectionProps).collection,
|
|
90
|
-
filters,
|
|
91
|
-
identifiers: metafields,
|
|
92
|
-
...sortShopify[sort],
|
|
93
|
-
});
|
|
94
|
-
shopifyProducts = data.collection?.products;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const baseUrl = url ?? new URL("https://localhost");
|
|
98
|
-
|
|
99
|
-
const products = shopifyProducts?.nodes.map((p) => toProduct(p, p.variants.nodes[0], baseUrl));
|
|
100
|
-
|
|
101
|
-
return products ?? [];
|
|
102
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import type { ProductListingPage } from "@decocms/apps-commerce/types";
|
|
2
|
-
import { getShopifyClient } from "../client";
|
|
3
|
-
import { ProductsByCollection, SearchProducts } from "../utils/storefront/queries";
|
|
4
|
-
import { type ProductShopify, toFilter, toProduct } from "../utils/transform";
|
|
5
|
-
import type { Metafield } from "../utils/types";
|
|
6
|
-
import {
|
|
7
|
-
getFiltersByUrl,
|
|
8
|
-
searchSortOptions,
|
|
9
|
-
searchSortShopify,
|
|
10
|
-
sortOptions,
|
|
11
|
-
sortShopify,
|
|
12
|
-
} from "../utils/utils";
|
|
13
|
-
|
|
14
|
-
interface PageInfo {
|
|
15
|
-
hasNextPage: boolean;
|
|
16
|
-
hasPreviousPage: boolean;
|
|
17
|
-
endCursor?: string;
|
|
18
|
-
startCursor?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface FilterNode {
|
|
22
|
-
id: string;
|
|
23
|
-
label: string;
|
|
24
|
-
type: string;
|
|
25
|
-
values: Array<{ id: string; label: string; count: number; input: string }>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface ProductConnection {
|
|
29
|
-
nodes: ProductShopify[];
|
|
30
|
-
pageInfo: PageInfo;
|
|
31
|
-
filters?: FilterNode[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface Props {
|
|
35
|
-
query?: string;
|
|
36
|
-
collectionName?: string;
|
|
37
|
-
count: number;
|
|
38
|
-
metafields?: Metafield[];
|
|
39
|
-
pageOffset?: number;
|
|
40
|
-
page?: number;
|
|
41
|
-
startCursor?: string;
|
|
42
|
-
endCursor?: string;
|
|
43
|
-
pageHref?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export default async function productListingPageLoader(
|
|
47
|
-
props: Props,
|
|
48
|
-
url?: URL,
|
|
49
|
-
): Promise<ProductListingPage | null> {
|
|
50
|
-
const pageUrl = url ?? new URL(props.pageHref || "https://localhost");
|
|
51
|
-
const client = getShopifyClient();
|
|
52
|
-
|
|
53
|
-
const count = props.count ?? 12;
|
|
54
|
-
const query = props.query || pageUrl.searchParams.get("q") || "";
|
|
55
|
-
const currentPageoffset = props.pageOffset ?? 1;
|
|
56
|
-
const pageParam = pageUrl.searchParams.get("page")
|
|
57
|
-
? Number(pageUrl.searchParams.get("page")) - currentPageoffset
|
|
58
|
-
: 0;
|
|
59
|
-
const page = props.page || pageParam;
|
|
60
|
-
const endCursor = props.endCursor || pageUrl.searchParams.get("endCursor") || "";
|
|
61
|
-
const startCursor = props.startCursor || pageUrl.searchParams.get("startCursor") || "";
|
|
62
|
-
const metafields = props.metafields || [];
|
|
63
|
-
|
|
64
|
-
const isSearch = Boolean(query);
|
|
65
|
-
let hasNextPage = false;
|
|
66
|
-
let hasPreviousPage = false;
|
|
67
|
-
let shopifyProducts: ProductConnection | undefined;
|
|
68
|
-
let shopifyFilters: FilterNode[] | undefined;
|
|
69
|
-
let records: number | undefined;
|
|
70
|
-
let collectionTitle: string | undefined;
|
|
71
|
-
let collectionDescription: string | undefined;
|
|
72
|
-
|
|
73
|
-
const sort = pageUrl.searchParams.get("sort") ?? "";
|
|
74
|
-
|
|
75
|
-
if (isSearch) {
|
|
76
|
-
const data = await client.query<{
|
|
77
|
-
search: ProductConnection & { totalCount?: number; productFilters?: FilterNode[] };
|
|
78
|
-
}>(SearchProducts, {
|
|
79
|
-
...(!endCursor && { first: count }),
|
|
80
|
-
...(endCursor && { last: count }),
|
|
81
|
-
...(startCursor && { after: startCursor }),
|
|
82
|
-
...(endCursor && { before: endCursor }),
|
|
83
|
-
query,
|
|
84
|
-
productFilters: getFiltersByUrl(pageUrl),
|
|
85
|
-
identifiers: metafields,
|
|
86
|
-
...searchSortShopify[sort],
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
shopifyProducts = data.search;
|
|
90
|
-
shopifyFilters = data.search?.productFilters;
|
|
91
|
-
records = data.search?.totalCount;
|
|
92
|
-
hasNextPage = Boolean(data.search?.pageInfo.hasNextPage);
|
|
93
|
-
hasPreviousPage = Boolean(data.search?.pageInfo.hasPreviousPage);
|
|
94
|
-
} else {
|
|
95
|
-
const pathname = props.collectionName || pageUrl.pathname.split("/")[1];
|
|
96
|
-
|
|
97
|
-
const data = await client.query<{
|
|
98
|
-
collection?: {
|
|
99
|
-
title?: string;
|
|
100
|
-
description?: string;
|
|
101
|
-
products: ProductConnection;
|
|
102
|
-
};
|
|
103
|
-
}>(ProductsByCollection, {
|
|
104
|
-
...(!endCursor && { first: count }),
|
|
105
|
-
...(endCursor && { last: count }),
|
|
106
|
-
...(startCursor && { after: startCursor }),
|
|
107
|
-
...(endCursor && { before: endCursor }),
|
|
108
|
-
identifiers: metafields,
|
|
109
|
-
handle: pathname,
|
|
110
|
-
filters: getFiltersByUrl(pageUrl),
|
|
111
|
-
...sortShopify[sort],
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
shopifyProducts = data.collection?.products;
|
|
115
|
-
shopifyFilters = data.collection?.products?.filters;
|
|
116
|
-
hasNextPage = Boolean(data.collection?.products.pageInfo.hasNextPage);
|
|
117
|
-
hasPreviousPage = Boolean(data.collection?.products.pageInfo.hasPreviousPage);
|
|
118
|
-
collectionTitle = data.collection?.title;
|
|
119
|
-
collectionDescription = data.collection?.description;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const products = shopifyProducts?.nodes?.map((p) => toProduct(p, p.variants.nodes[0], pageUrl));
|
|
123
|
-
|
|
124
|
-
const nextPage = new URLSearchParams(pageUrl.searchParams);
|
|
125
|
-
const previousPage = new URLSearchParams(pageUrl.searchParams);
|
|
126
|
-
|
|
127
|
-
if (hasNextPage) {
|
|
128
|
-
nextPage.set("page", (page + currentPageoffset + 1).toString());
|
|
129
|
-
nextPage.set("startCursor", shopifyProducts?.pageInfo.endCursor ?? "");
|
|
130
|
-
nextPage.delete("endCursor");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (hasPreviousPage) {
|
|
134
|
-
previousPage.set("page", (page + currentPageoffset - 1).toString());
|
|
135
|
-
previousPage.set("endCursor", shopifyProducts?.pageInfo.startCursor ?? "");
|
|
136
|
-
previousPage.delete("startCursor");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const filters = shopifyFilters?.map((filter) => toFilter(filter, pageUrl));
|
|
140
|
-
const currentPage = page + currentPageoffset;
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
"@type": "ProductListingPage",
|
|
144
|
-
breadcrumb: {
|
|
145
|
-
"@type": "BreadcrumbList",
|
|
146
|
-
itemListElement: [
|
|
147
|
-
{
|
|
148
|
-
"@type": "ListItem" as const,
|
|
149
|
-
name: isSearch ? query : pageUrl.pathname.split("/")[1],
|
|
150
|
-
item: isSearch ? pageUrl.href : pageUrl.pathname,
|
|
151
|
-
position: 2,
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
numberOfItems: 1,
|
|
155
|
-
},
|
|
156
|
-
filters: filters ?? [],
|
|
157
|
-
products: products ?? [],
|
|
158
|
-
pageInfo: {
|
|
159
|
-
nextPage: hasNextPage ? `?${nextPage}` : undefined,
|
|
160
|
-
previousPage: hasPreviousPage ? `?${previousPage}` : undefined,
|
|
161
|
-
currentPage,
|
|
162
|
-
records,
|
|
163
|
-
recordPerPage: count,
|
|
164
|
-
},
|
|
165
|
-
sortOptions: isSearch ? searchSortOptions : sortOptions,
|
|
166
|
-
seo: {
|
|
167
|
-
title: collectionTitle || "",
|
|
168
|
-
description: collectionDescription || "",
|
|
169
|
-
canonical: `${pageUrl.origin}${pageUrl.pathname}${page >= 1 ? `?page=${page}` : ""}`,
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Product } from "@decocms/apps-commerce/types";
|
|
2
|
-
import { getShopifyClient } from "../client";
|
|
3
|
-
import { GetProduct, ProductRecommendations } from "../utils/storefront/queries";
|
|
4
|
-
import { type ProductShopify, toProduct } from "../utils/transform";
|
|
5
|
-
import type { Metafield } from "../utils/types";
|
|
6
|
-
|
|
7
|
-
export interface Props {
|
|
8
|
-
slug: string;
|
|
9
|
-
count?: number;
|
|
10
|
-
metafields?: Metafield[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default async function relatedProductsLoader(
|
|
14
|
-
props: Props,
|
|
15
|
-
url?: URL,
|
|
16
|
-
): Promise<Product[] | null> {
|
|
17
|
-
const client = getShopifyClient();
|
|
18
|
-
const { slug, count = 10, metafields = [] } = props;
|
|
19
|
-
|
|
20
|
-
const splitted = slug?.split("-") ?? [];
|
|
21
|
-
const maybeSkuId = Number(splitted[splitted.length - 1]);
|
|
22
|
-
const handle = splitted.slice(0, maybeSkuId ? -1 : undefined).join("-");
|
|
23
|
-
|
|
24
|
-
const productData = await client.query<{ product?: ProductShopify }>(GetProduct, {
|
|
25
|
-
handle,
|
|
26
|
-
identifiers: metafields,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (!productData?.product) return [];
|
|
30
|
-
|
|
31
|
-
const data = await client.query<{
|
|
32
|
-
productRecommendations?: ProductShopify[];
|
|
33
|
-
}>(ProductRecommendations, { productId: productData.product.id, identifiers: metafields });
|
|
34
|
-
|
|
35
|
-
if (!data?.productRecommendations) return [];
|
|
36
|
-
|
|
37
|
-
const baseUrl = url ?? new URL("https://localhost");
|
|
38
|
-
|
|
39
|
-
return data.productRecommendations
|
|
40
|
-
.map((p) => toProduct(p, p.variants.nodes[0], baseUrl))
|
|
41
|
-
.slice(0, count);
|
|
42
|
-
}
|
package/src/loaders/cart.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { getShopifyClient } from "../client";
|
|
2
|
-
import { getCartCookie, setCartCookie } from "../utils/cart";
|
|
3
|
-
import { CreateCart, GetCart } from "../utils/storefront/queries";
|
|
4
|
-
|
|
5
|
-
export interface CartLine {
|
|
6
|
-
id: string;
|
|
7
|
-
quantity: number;
|
|
8
|
-
merchandise: {
|
|
9
|
-
id: string;
|
|
10
|
-
title: string;
|
|
11
|
-
image?: { url: string; altText?: string | null } | null;
|
|
12
|
-
product: { title: string; handle: string; onlineStoreUrl?: string | null };
|
|
13
|
-
price: { amount: string; currencyCode: string };
|
|
14
|
-
};
|
|
15
|
-
discountAllocations?: Array<{
|
|
16
|
-
code?: string;
|
|
17
|
-
discountedAmount?: { amount: string; currencyCode: string };
|
|
18
|
-
}>;
|
|
19
|
-
cost?: {
|
|
20
|
-
totalAmount: { amount: string; currencyCode: string };
|
|
21
|
-
subtotalAmount: { amount: string; currencyCode: string };
|
|
22
|
-
amountPerQuantity?: { amount: string; currencyCode: string };
|
|
23
|
-
compareAtAmountPerQuantity?: { amount: string; currencyCode: string } | null;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ShopifyCart {
|
|
28
|
-
id: string;
|
|
29
|
-
checkoutUrl: string;
|
|
30
|
-
totalQuantity: number;
|
|
31
|
-
lines: { nodes: CartLine[] };
|
|
32
|
-
cost: {
|
|
33
|
-
totalTaxAmount?: { amount: string; currencyCode: string };
|
|
34
|
-
subtotalAmount: { amount: string; currencyCode: string };
|
|
35
|
-
totalAmount: { amount: string; currencyCode: string };
|
|
36
|
-
checkoutChargeAmount?: { amount: string; currencyCode: string };
|
|
37
|
-
};
|
|
38
|
-
discountCodes?: Array<{ applicable: boolean; code: string }>;
|
|
39
|
-
discountAllocations?: Array<{
|
|
40
|
-
discountedAmount: { amount: string; currencyCode: string };
|
|
41
|
-
}>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function getCart(
|
|
45
|
-
requestHeaders: Headers,
|
|
46
|
-
responseHeaders?: Headers,
|
|
47
|
-
): Promise<ShopifyCart | null> {
|
|
48
|
-
const client = getShopifyClient();
|
|
49
|
-
const maybeCartId = getCartCookie(requestHeaders);
|
|
50
|
-
|
|
51
|
-
const cartId =
|
|
52
|
-
maybeCartId ??
|
|
53
|
-
(await client
|
|
54
|
-
.query<{ payload?: { cart?: { id: string } } }>(CreateCart)
|
|
55
|
-
.then((data) => data.payload?.cart?.id));
|
|
56
|
-
|
|
57
|
-
if (!cartId) throw new Error("Missing cart id");
|
|
58
|
-
|
|
59
|
-
const cart = await client
|
|
60
|
-
.query<{ cart?: ShopifyCart }>(GetCart, { id: decodeURIComponent(cartId) })
|
|
61
|
-
.then((data) => data.cart ?? null);
|
|
62
|
-
|
|
63
|
-
if (responseHeaders) {
|
|
64
|
-
setCartCookie(responseHeaders, cartId);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return cart;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function createCart(): Promise<string | null> {
|
|
71
|
-
const client = getShopifyClient();
|
|
72
|
-
const data = await client.query<{ payload?: { cart?: { id: string } } }>(CreateCart);
|
|
73
|
-
return data?.payload?.cart?.id ?? null;
|
|
74
|
-
}
|
package/src/loaders/shop.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { getShopifyClient } from "../client";
|
|
2
|
-
import { GetShopInfo } from "../utils/storefront/queries";
|
|
3
|
-
import type { Metafield } from "../utils/types";
|
|
4
|
-
|
|
5
|
-
export interface Shop {
|
|
6
|
-
name: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
privacyPolicy?: { title: string; body: string };
|
|
9
|
-
refundPolicy?: { title: string; body: string };
|
|
10
|
-
shippingPolicy?: { title: string; body: string };
|
|
11
|
-
subscriptionPolicy?: { title: string; body: string };
|
|
12
|
-
termsOfService?: { title: string; body: string };
|
|
13
|
-
metafields?: Array<{
|
|
14
|
-
description?: string | null;
|
|
15
|
-
key: string;
|
|
16
|
-
namespace: string;
|
|
17
|
-
type: string;
|
|
18
|
-
value: string;
|
|
19
|
-
reference?: { image?: { url: string } } | null;
|
|
20
|
-
references?: { edges: Array<{ node: { image?: { url: string } } }> } | null;
|
|
21
|
-
} | null>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Props {
|
|
25
|
-
metafields?: Metafield[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default async function shopLoader(props?: Props): Promise<Shop> {
|
|
29
|
-
const client = getShopifyClient();
|
|
30
|
-
const metafields = props?.metafields || [];
|
|
31
|
-
|
|
32
|
-
const data = await client.query<{ shop: Shop }>(GetShopInfo, { identifiers: metafields });
|
|
33
|
-
|
|
34
|
-
return data.shop;
|
|
35
|
-
}
|
package/src/loaders/user.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { getShopifyClient } from "../client";
|
|
2
|
-
import { FetchCustomerInfo } from "../utils/storefront/queries";
|
|
3
|
-
import { getUserCookie } from "../utils/user";
|
|
4
|
-
|
|
5
|
-
export interface ShopifyUser {
|
|
6
|
-
"@id": string;
|
|
7
|
-
email: string;
|
|
8
|
-
givenName: string;
|
|
9
|
-
familyName: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default async function userLoader(requestHeaders: Headers): Promise<ShopifyUser | null> {
|
|
13
|
-
const client = getShopifyClient();
|
|
14
|
-
const customerAccessToken = getUserCookie(requestHeaders);
|
|
15
|
-
|
|
16
|
-
if (!customerAccessToken) return null;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const data = await client.query<{
|
|
20
|
-
customer?: {
|
|
21
|
-
id: string;
|
|
22
|
-
email?: string | null;
|
|
23
|
-
firstName?: string | null;
|
|
24
|
-
lastName?: string | null;
|
|
25
|
-
};
|
|
26
|
-
}>(FetchCustomerInfo, { customerAccessToken });
|
|
27
|
-
|
|
28
|
-
if (!data.customer) return null;
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
"@id": data.customer.id,
|
|
32
|
-
email: data.customer.email ?? "",
|
|
33
|
-
givenName: data.customer.firstName ?? "",
|
|
34
|
-
familyName: data.customer.lastName ?? "",
|
|
35
|
-
};
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
package/src/manifest.gen.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
|
|
2
|
-
// This file is checked into source control and updated via: npm run generate:manifests
|
|
3
|
-
|
|
4
|
-
import * as actions_cart_addItems from "./actions/cart/addItems";
|
|
5
|
-
import * as actions_cart_updateCoupons from "./actions/cart/updateCoupons";
|
|
6
|
-
import * as actions_cart_updateItems from "./actions/cart/updateItems";
|
|
7
|
-
import * as actions_user_signIn from "./actions/user/signIn";
|
|
8
|
-
import * as actions_user_signUp from "./actions/user/signUp";
|
|
9
|
-
import * as loaders_cart from "./loaders/cart";
|
|
10
|
-
import * as loaders_ProductDetailsPage from "./loaders/ProductDetailsPage";
|
|
11
|
-
import * as loaders_ProductList from "./loaders/ProductList";
|
|
12
|
-
import * as loaders_ProductListingPage from "./loaders/ProductListingPage";
|
|
13
|
-
import * as loaders_RelatedProducts from "./loaders/RelatedProducts";
|
|
14
|
-
import * as loaders_shop from "./loaders/shop";
|
|
15
|
-
import * as loaders_user from "./loaders/user";
|
|
16
|
-
|
|
17
|
-
const manifest = {
|
|
18
|
-
name: "shopify",
|
|
19
|
-
loaders: {
|
|
20
|
-
"shopify/loaders/ProductDetailsPage": loaders_ProductDetailsPage,
|
|
21
|
-
"shopify/loaders/ProductList": loaders_ProductList,
|
|
22
|
-
"shopify/loaders/ProductListingPage": loaders_ProductListingPage,
|
|
23
|
-
"shopify/loaders/RelatedProducts": loaders_RelatedProducts,
|
|
24
|
-
"shopify/loaders/cart": loaders_cart,
|
|
25
|
-
"shopify/loaders/shop": loaders_shop,
|
|
26
|
-
"shopify/loaders/user": loaders_user,
|
|
27
|
-
},
|
|
28
|
-
actions: {
|
|
29
|
-
"shopify/actions/cart/addItems": actions_cart_addItems,
|
|
30
|
-
"shopify/actions/cart/updateCoupons": actions_cart_updateCoupons,
|
|
31
|
-
"shopify/actions/cart/updateItems": actions_cart_updateItems,
|
|
32
|
-
"shopify/actions/user/signIn": actions_user_signIn,
|
|
33
|
-
"shopify/actions/user/signUp": actions_user_signUp,
|
|
34
|
-
},
|
|
35
|
-
sections: {},
|
|
36
|
-
} as const;
|
|
37
|
-
|
|
38
|
-
export type Manifest = typeof manifest;
|
|
39
|
-
export default manifest;
|
package/src/mod.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify app module — standard autoconfig contract.
|
|
3
|
-
*
|
|
4
|
-
* Exports `configure` following the AppModContract pattern.
|
|
5
|
-
* The framework's `autoconfigApps()` calls these generically.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import * as shopifyApp from "@decocms/apps/shopify/mod";
|
|
10
|
-
*
|
|
11
|
-
* const app = await shopifyApp.configure(blocks["deco-shopify"], resolveSecret);
|
|
12
|
-
* if (app) {
|
|
13
|
-
* // app.manifest, app.state are available
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { AppDefinition, ResolveSecretFn } from "@decocms/apps-commerce/app-types";
|
|
19
|
-
import { configureShopify, type ShopifyConfig } from "./client";
|
|
20
|
-
import manifest from "./manifest.gen";
|
|
21
|
-
|
|
22
|
-
// -------------------------------------------------------------------------
|
|
23
|
-
// State
|
|
24
|
-
// -------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
export interface ShopifyState {
|
|
27
|
-
config: ShopifyConfig;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// -------------------------------------------------------------------------
|
|
31
|
-
// Configure
|
|
32
|
-
// -------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Configure the Shopify app from CMS block data.
|
|
36
|
-
* Returns an AppDefinition or null if required fields are missing.
|
|
37
|
-
*/
|
|
38
|
-
export async function configure(
|
|
39
|
-
block: Record<string, unknown>,
|
|
40
|
-
resolveSecret: ResolveSecretFn,
|
|
41
|
-
): Promise<AppDefinition<ShopifyState> | null> {
|
|
42
|
-
if (!block?.storeName) return null;
|
|
43
|
-
|
|
44
|
-
const storefrontAccessToken =
|
|
45
|
-
(await resolveSecret(block.storefrontAccessToken, "SHOPIFY_STOREFRONT_TOKEN")) ??
|
|
46
|
-
(typeof block.storefrontAccessToken === "string" ? block.storefrontAccessToken : null);
|
|
47
|
-
|
|
48
|
-
if (!storefrontAccessToken) return null;
|
|
49
|
-
|
|
50
|
-
const config: ShopifyConfig = {
|
|
51
|
-
storeName: block.storeName as string,
|
|
52
|
-
storefrontAccessToken,
|
|
53
|
-
publicUrl: block.publicUrl as string | undefined,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Bridge: maintain global singleton for backward compat
|
|
57
|
-
configureShopify(config);
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
name: "shopify",
|
|
61
|
-
manifest,
|
|
62
|
-
state: { config },
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Placeholder preview for CMS editor — evolves when admin supports it. */
|
|
67
|
-
export const preview = undefined;
|
package/src/registry.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { AppRegistryEntry } from "@decocms/apps-commerce/registry";
|
|
2
|
-
|
|
3
|
-
export const SHOPIFY_REGISTRY_ENTRY: AppRegistryEntry = {
|
|
4
|
-
blockKey: "deco-shopify",
|
|
5
|
-
module: () => import("./mod"),
|
|
6
|
-
displayName: "Shopify",
|
|
7
|
-
category: "commerce",
|
|
8
|
-
description: "Shopify Storefront API commerce integration",
|
|
9
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { extractGraphqlOperationName } from "../graphqlOperationName";
|
|
3
|
-
|
|
4
|
-
describe("extractGraphqlOperationName", () => {
|
|
5
|
-
it("returns the explicit name when provided, regardless of body content", () => {
|
|
6
|
-
expect(extractGraphqlOperationName("query Whatever { x }", "ForcedName")).toBe("ForcedName");
|
|
7
|
-
expect(extractGraphqlOperationName("", "Override")).toBe("Override");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("extracts a single named query", () => {
|
|
11
|
-
expect(
|
|
12
|
-
extractGraphqlOperationName("query ProductBySlug($slug: String!) { product { id } }"),
|
|
13
|
-
).toBe("ProductBySlug");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("extracts a single named mutation", () => {
|
|
17
|
-
expect(
|
|
18
|
-
extractGraphqlOperationName("mutation CartLinesAdd($cartId: ID!) { cartLinesAdd { } }"),
|
|
19
|
-
).toBe("CartLinesAdd");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("extracts a single named subscription", () => {
|
|
23
|
-
expect(extractGraphqlOperationName("subscription OrderEvents { orderUpdated { id } }")).toBe(
|
|
24
|
-
"OrderEvents",
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("returns undefined for anonymous operations", () => {
|
|
29
|
-
expect(extractGraphqlOperationName("{ product { id } }")).toBeUndefined();
|
|
30
|
-
expect(extractGraphqlOperationName("query { product { id } }")).toBeUndefined();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns undefined when document has more than one named operation (caller must disambiguate)", () => {
|
|
34
|
-
const multi = `
|
|
35
|
-
query OpA { a }
|
|
36
|
-
query OpB { b }
|
|
37
|
-
`;
|
|
38
|
-
expect(extractGraphqlOperationName(multi)).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("ignores the words query/mutation/subscription inside string literals", () => {
|
|
42
|
-
const docWithStringy = `query RealName { thing(arg: "this query mutation subscription is a string") }`;
|
|
43
|
-
expect(extractGraphqlOperationName(docWithStringy)).toBe("RealName");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("ignores the words inside block strings (triple-quoted)", () => {
|
|
47
|
-
const doc = `
|
|
48
|
-
"""
|
|
49
|
-
This block string mentions query Inner and mutation Inner2.
|
|
50
|
-
"""
|
|
51
|
-
query OuterReal { x }
|
|
52
|
-
`;
|
|
53
|
-
expect(extractGraphqlOperationName(doc)).toBe("OuterReal");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("ignores the words inside # comments", () => {
|
|
57
|
-
const doc = `
|
|
58
|
-
# query CommentedOut { x }
|
|
59
|
-
query Active { y }
|
|
60
|
-
`;
|
|
61
|
-
expect(extractGraphqlOperationName(doc)).toBe("Active");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns undefined on an empty / nullish body", () => {
|
|
65
|
-
expect(extractGraphqlOperationName("")).toBeUndefined();
|
|
66
|
-
expect(extractGraphqlOperationName(" \n\t ")).toBeUndefined();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("handles a real-world Shopify storefront query shape", () => {
|
|
70
|
-
const doc = `
|
|
71
|
-
query ProductDetails($handle: String!, $country: CountryCode!) @inContext(country: $country) {
|
|
72
|
-
product(handle: $handle) {
|
|
73
|
-
id
|
|
74
|
-
title
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
`;
|
|
78
|
-
expect(extractGraphqlOperationName(doc)).toBe("ProductDetails");
|
|
79
|
-
});
|
|
80
|
-
});
|