@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.
Files changed (113) hide show
  1. package/.github/workflows/release.yml +34 -0
  2. package/.releaserc.json +25 -0
  3. package/commerce/components/Image.tsx +209 -0
  4. package/commerce/components/JsonLd.tsx +285 -0
  5. package/commerce/sdk/analytics.ts +24 -0
  6. package/commerce/sdk/formatPrice.ts +23 -0
  7. package/commerce/sdk/url.ts +9 -0
  8. package/commerce/sdk/useOffer.ts +75 -0
  9. package/commerce/sdk/useVariantPossibilities.ts +43 -0
  10. package/commerce/types/commerce.ts +1105 -0
  11. package/commerce/utils/canonical.ts +11 -0
  12. package/commerce/utils/constants.ts +9 -0
  13. package/commerce/utils/filters.ts +10 -0
  14. package/commerce/utils/productToAnalyticsItem.ts +67 -0
  15. package/commerce/utils/stateByZip.ts +50 -0
  16. package/knip.json +19 -0
  17. package/package.json +77 -0
  18. package/shopify/actions/cart/addItems.ts +37 -0
  19. package/shopify/actions/cart/updateCoupons.ts +32 -0
  20. package/shopify/actions/cart/updateItems.ts +32 -0
  21. package/shopify/actions/user/signIn.ts +45 -0
  22. package/shopify/actions/user/signUp.ts +36 -0
  23. package/shopify/client.ts +58 -0
  24. package/shopify/index.ts +32 -0
  25. package/shopify/init.ts +40 -0
  26. package/shopify/loaders/ProductDetailsPage.ts +35 -0
  27. package/shopify/loaders/ProductList.ts +101 -0
  28. package/shopify/loaders/ProductListingPage.ts +180 -0
  29. package/shopify/loaders/RelatedProducts.ts +45 -0
  30. package/shopify/loaders/cart.ts +73 -0
  31. package/shopify/loaders/shop.ts +40 -0
  32. package/shopify/loaders/user.ts +44 -0
  33. package/shopify/utils/admin/admin.ts +57 -0
  34. package/shopify/utils/admin/queries.ts +29 -0
  35. package/shopify/utils/cart.ts +28 -0
  36. package/shopify/utils/cookies.ts +85 -0
  37. package/shopify/utils/enums.ts +438 -0
  38. package/shopify/utils/graphql.ts +69 -0
  39. package/shopify/utils/storefront/queries.ts +530 -0
  40. package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
  41. package/shopify/utils/transform.ts +436 -0
  42. package/shopify/utils/types.ts +191 -0
  43. package/shopify/utils/user.ts +23 -0
  44. package/shopify/utils/utils.ts +164 -0
  45. package/tsconfig.json +11 -0
  46. package/vtex/README.md +6 -0
  47. package/vtex/actions/address.ts +211 -0
  48. package/vtex/actions/auth.ts +337 -0
  49. package/vtex/actions/checkout.ts +497 -0
  50. package/vtex/actions/index.ts +11 -0
  51. package/vtex/actions/masterData.ts +170 -0
  52. package/vtex/actions/misc.ts +196 -0
  53. package/vtex/actions/newsletter.ts +108 -0
  54. package/vtex/actions/orders.ts +37 -0
  55. package/vtex/actions/profile.ts +119 -0
  56. package/vtex/actions/session.ts +87 -0
  57. package/vtex/actions/trigger.ts +43 -0
  58. package/vtex/actions/wishlist.ts +116 -0
  59. package/vtex/client.ts +423 -0
  60. package/vtex/hooks/index.ts +4 -0
  61. package/vtex/hooks/useAutocomplete.ts +89 -0
  62. package/vtex/hooks/useCart.ts +219 -0
  63. package/vtex/hooks/useUser.ts +78 -0
  64. package/vtex/hooks/useWishlist.ts +119 -0
  65. package/vtex/index.ts +14 -0
  66. package/vtex/inline-loaders/productDetailsPage.ts +75 -0
  67. package/vtex/inline-loaders/productList.ts +163 -0
  68. package/vtex/inline-loaders/productListingPage.ts +447 -0
  69. package/vtex/inline-loaders/relatedProducts.ts +83 -0
  70. package/vtex/inline-loaders/suggestions.ts +49 -0
  71. package/vtex/inline-loaders/workflowProducts.ts +68 -0
  72. package/vtex/invoke.ts +202 -0
  73. package/vtex/loaders/address.ts +120 -0
  74. package/vtex/loaders/brands.ts +51 -0
  75. package/vtex/loaders/cart.ts +49 -0
  76. package/vtex/loaders/catalog.ts +165 -0
  77. package/vtex/loaders/collections.ts +57 -0
  78. package/vtex/loaders/index.ts +19 -0
  79. package/vtex/loaders/legacy.ts +671 -0
  80. package/vtex/loaders/logistics.ts +115 -0
  81. package/vtex/loaders/navbar.ts +29 -0
  82. package/vtex/loaders/orders.ts +103 -0
  83. package/vtex/loaders/pageType.ts +62 -0
  84. package/vtex/loaders/payment.ts +107 -0
  85. package/vtex/loaders/profile.ts +138 -0
  86. package/vtex/loaders/promotion.ts +33 -0
  87. package/vtex/loaders/search.ts +127 -0
  88. package/vtex/loaders/session.ts +91 -0
  89. package/vtex/loaders/user.ts +89 -0
  90. package/vtex/loaders/wishlist.ts +89 -0
  91. package/vtex/loaders/wishlistProducts.ts +81 -0
  92. package/vtex/loaders/workflow.ts +323 -0
  93. package/vtex/logo.png +0 -0
  94. package/vtex/middleware.ts +229 -0
  95. package/vtex/types.ts +248 -0
  96. package/vtex/utils/batch.ts +21 -0
  97. package/vtex/utils/cookies.ts +76 -0
  98. package/vtex/utils/enrichment.ts +540 -0
  99. package/vtex/utils/fetchCache.ts +150 -0
  100. package/vtex/utils/index.ts +17 -0
  101. package/vtex/utils/intelligentSearch.ts +84 -0
  102. package/vtex/utils/legacy.ts +155 -0
  103. package/vtex/utils/pickAndOmit.ts +30 -0
  104. package/vtex/utils/proxy.ts +196 -0
  105. package/vtex/utils/resourceRange.ts +10 -0
  106. package/vtex/utils/segment.ts +163 -0
  107. package/vtex/utils/similars.ts +38 -0
  108. package/vtex/utils/sitemap.ts +133 -0
  109. package/vtex/utils/slugCache.ts +32 -0
  110. package/vtex/utils/slugify.ts +13 -0
  111. package/vtex/utils/transform.ts +1331 -0
  112. package/vtex/utils/types.ts +1884 -0
  113. package/vtex/utils/vtexId.ts +103 -0
@@ -0,0 +1,101 @@
1
+ import type { Product } from "../../commerce/types/commerce";
2
+ import { getShopifyClient } from "../client";
3
+ import { ProductsByCollection, SearchProducts } from "../utils/storefront/queries";
4
+ import { toProduct, type ProductShopify } 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: any): p is QueryProps =>
41
+ 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 ??
50
+ (expandedProps as unknown as Props["props"]);
51
+
52
+ const count = props.count ?? 12;
53
+ const metafields = expandedProps.metafields || [];
54
+ const sort = props.sort ?? "";
55
+
56
+ const filters: any[] = [];
57
+ expandedProps.filters?.tags?.forEach((tag) => filters.push({ tag }));
58
+ expandedProps.filters?.productTypes?.forEach((productType) => filters.push({ productType }));
59
+ expandedProps.filters?.productVendors?.forEach((productVendor) => filters.push({ productVendor }));
60
+ if (expandedProps.filters?.priceMin) filters.push({ price: { min: expandedProps.filters.priceMin } });
61
+ if (expandedProps.filters?.priceMax) filters.push({ price: { max: expandedProps.filters.priceMax } });
62
+ expandedProps.filters?.variantOptions?.forEach((variantOption) => filters.push({ variantOption }));
63
+
64
+ let shopifyProducts: { nodes: ProductShopify[] } | undefined;
65
+
66
+ if (isQueryList(props)) {
67
+ const data = await client.query<{ search: { nodes: ProductShopify[] } }>(
68
+ SearchProducts,
69
+ {
70
+ first: count,
71
+ query: props.query,
72
+ productFilters: filters,
73
+ identifiers: metafields,
74
+ ...searchSortShopify[sort],
75
+ },
76
+ );
77
+ shopifyProducts = data.search;
78
+ } else {
79
+ const data = await client.query<{
80
+ collection?: { products: { nodes: ProductShopify[] } };
81
+ }>(
82
+ ProductsByCollection,
83
+ {
84
+ first: count,
85
+ handle: (props as CollectionProps).collection,
86
+ filters,
87
+ identifiers: metafields,
88
+ ...sortShopify[sort],
89
+ },
90
+ );
91
+ shopifyProducts = data.collection?.products;
92
+ }
93
+
94
+ const baseUrl = url ?? new URL("https://localhost");
95
+
96
+ const products = shopifyProducts?.nodes.map((p) =>
97
+ toProduct(p, p.variants.nodes[0], baseUrl)
98
+ );
99
+
100
+ return products ?? [];
101
+ }
@@ -0,0 +1,180 @@
1
+ import type { ProductListingPage } from "../../commerce/types/commerce";
2
+ import { getShopifyClient } from "../client";
3
+ import { ProductsByCollection, SearchProducts } from "../utils/storefront/queries";
4
+ import { toFilter, toProduct, type ProductShopify } 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
+ }>(
79
+ SearchProducts,
80
+ {
81
+ ...(!endCursor && { first: count }),
82
+ ...(endCursor && { last: count }),
83
+ ...(startCursor && { after: startCursor }),
84
+ ...(endCursor && { before: endCursor }),
85
+ query,
86
+ productFilters: getFiltersByUrl(pageUrl),
87
+ identifiers: metafields,
88
+ ...searchSortShopify[sort],
89
+ },
90
+ );
91
+
92
+ shopifyProducts = data.search;
93
+ shopifyFilters = data.search?.productFilters;
94
+ records = data.search?.totalCount;
95
+ hasNextPage = Boolean(data.search?.pageInfo.hasNextPage);
96
+ hasPreviousPage = Boolean(data.search?.pageInfo.hasPreviousPage);
97
+ } else {
98
+ const pathname = props.collectionName || pageUrl.pathname.split("/")[1];
99
+
100
+ const data = await client.query<{
101
+ collection?: {
102
+ title?: string;
103
+ description?: string;
104
+ products: ProductConnection;
105
+ };
106
+ }>(
107
+ ProductsByCollection,
108
+ {
109
+ ...(!endCursor && { first: count }),
110
+ ...(endCursor && { last: count }),
111
+ ...(startCursor && { after: startCursor }),
112
+ ...(endCursor && { before: endCursor }),
113
+ identifiers: metafields,
114
+ handle: pathname,
115
+ filters: getFiltersByUrl(pageUrl),
116
+ ...sortShopify[sort],
117
+ },
118
+ );
119
+
120
+ shopifyProducts = data.collection?.products;
121
+ shopifyFilters = data.collection?.products?.filters;
122
+ hasNextPage = Boolean(data.collection?.products.pageInfo.hasNextPage);
123
+ hasPreviousPage = Boolean(data.collection?.products.pageInfo.hasPreviousPage);
124
+ collectionTitle = data.collection?.title;
125
+ collectionDescription = data.collection?.description;
126
+ }
127
+
128
+ const products = shopifyProducts?.nodes?.map((p) =>
129
+ toProduct(p, p.variants.nodes[0], pageUrl)
130
+ );
131
+
132
+ const nextPage = new URLSearchParams(pageUrl.searchParams);
133
+ const previousPage = new URLSearchParams(pageUrl.searchParams);
134
+
135
+ if (hasNextPage) {
136
+ nextPage.set("page", (page + currentPageoffset + 1).toString());
137
+ nextPage.set("startCursor", shopifyProducts?.pageInfo.endCursor ?? "");
138
+ nextPage.delete("endCursor");
139
+ }
140
+
141
+ if (hasPreviousPage) {
142
+ previousPage.set("page", (page + currentPageoffset - 1).toString());
143
+ previousPage.set("endCursor", shopifyProducts?.pageInfo.startCursor ?? "");
144
+ previousPage.delete("startCursor");
145
+ }
146
+
147
+ const filters = shopifyFilters?.map((filter) => toFilter(filter, pageUrl));
148
+ const currentPage = page + currentPageoffset;
149
+
150
+ return {
151
+ "@type": "ProductListingPage",
152
+ breadcrumb: {
153
+ "@type": "BreadcrumbList",
154
+ itemListElement: [{
155
+ "@type": "ListItem" as const,
156
+ name: isSearch ? query : pageUrl.pathname.split("/")[1],
157
+ item: isSearch ? pageUrl.href : pageUrl.pathname,
158
+ position: 2,
159
+ }],
160
+ numberOfItems: 1,
161
+ },
162
+ filters: filters ?? [],
163
+ products: products ?? [],
164
+ pageInfo: {
165
+ nextPage: hasNextPage ? `?${nextPage}` : undefined,
166
+ previousPage: hasPreviousPage ? `?${previousPage}` : undefined,
167
+ currentPage,
168
+ records,
169
+ recordPerPage: count,
170
+ },
171
+ sortOptions: isSearch ? searchSortOptions : sortOptions,
172
+ seo: {
173
+ title: collectionTitle || "",
174
+ description: collectionDescription || "",
175
+ canonical: `${pageUrl.origin}${pageUrl.pathname}${
176
+ page >= 1 ? `?page=${page}` : ""
177
+ }`,
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,45 @@
1
+ import type { Product } from "../../commerce/types/commerce";
2
+ import { getShopifyClient } from "../client";
3
+ import { GetProduct, ProductRecommendations } from "../utils/storefront/queries";
4
+ import { toProduct, type ProductShopify } 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 }>(
25
+ GetProduct,
26
+ { handle, identifiers: metafields },
27
+ );
28
+
29
+ if (!productData?.product) return [];
30
+
31
+ const data = await client.query<{
32
+ productRecommendations?: ProductShopify[];
33
+ }>(
34
+ ProductRecommendations,
35
+ { productId: productData.product.id, identifiers: metafields },
36
+ );
37
+
38
+ if (!data?.productRecommendations) return [];
39
+
40
+ const baseUrl = url ?? new URL("https://localhost");
41
+
42
+ return data.productRecommendations
43
+ .map((p) => toProduct(p, p.variants.nodes[0], baseUrl))
44
+ .slice(0, count);
45
+ }
@@ -0,0 +1,73 @@
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 = maybeCartId ??
52
+ await client.query<{ payload?: { cart?: { id: string } } }>(CreateCart)
53
+ .then((data) => data.payload?.cart?.id);
54
+
55
+ if (!cartId) throw new Error("Missing cart id");
56
+
57
+ const cart = await client.query<{ cart?: ShopifyCart }>(
58
+ GetCart,
59
+ { id: decodeURIComponent(cartId) },
60
+ ).then((data) => data.cart ?? null);
61
+
62
+ if (responseHeaders) {
63
+ setCartCookie(responseHeaders, cartId);
64
+ }
65
+
66
+ return cart;
67
+ }
68
+
69
+ export async function createCart(): Promise<string | null> {
70
+ const client = getShopifyClient();
71
+ const data = await client.query<{ payload?: { cart?: { id: string } } }>(CreateCart);
72
+ return data?.payload?.cart?.id ?? null;
73
+ }
@@ -0,0 +1,40 @@
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(
29
+ props?: Props,
30
+ ): Promise<Shop> {
31
+ const client = getShopifyClient();
32
+ const metafields = props?.metafields || [];
33
+
34
+ const data = await client.query<{ shop: Shop }>(
35
+ GetShopInfo,
36
+ { identifiers: metafields },
37
+ );
38
+
39
+ return data.shop;
40
+ }
@@ -0,0 +1,44 @@
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(
13
+ requestHeaders: Headers,
14
+ ): Promise<ShopifyUser | null> {
15
+ const client = getShopifyClient();
16
+ const customerAccessToken = getUserCookie(requestHeaders);
17
+
18
+ if (!customerAccessToken) return null;
19
+
20
+ try {
21
+ const data = await client.query<{
22
+ customer?: {
23
+ id: string;
24
+ email?: string | null;
25
+ firstName?: string | null;
26
+ lastName?: string | null;
27
+ };
28
+ }>(
29
+ FetchCustomerInfo,
30
+ { customerAccessToken },
31
+ );
32
+
33
+ if (!data.customer) return null;
34
+
35
+ return {
36
+ "@id": data.customer.id,
37
+ email: data.customer.email ?? "",
38
+ givenName: data.customer.firstName ?? "",
39
+ familyName: data.customer.lastName ?? "",
40
+ };
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
@@ -0,0 +1,57 @@
1
+
2
+ // deno-fmt-ignore-file
3
+ // deno-lint-ignore-file no-explicit-any ban-types ban-unused-ignore
4
+
5
+ export type Maybe<T> = T | null;
6
+
7
+ export type Scalars = {
8
+ ID: { input: string; output: string };
9
+ String: { input: string; output: string };
10
+ Boolean: { input: boolean; output: boolean };
11
+ Int: { input: number; output: number };
12
+ Float: { input: number; output: number };
13
+ ARN: { input: any; output: any };
14
+ Date: { input: any; output: any };
15
+ DateTime: { input: any; output: any };
16
+ Decimal: { input: any; output: any };
17
+ FormattedString: { input: any; output: any };
18
+ HTML: { input: any; output: any };
19
+ JSON: { input: any; output: any };
20
+ Money: { input: any; output: any };
21
+ StorefrontID: { input: any; output: any };
22
+ URL: { input: any; output: any };
23
+ UnsignedInt64: { input: any; output: any };
24
+ UtcOffset: { input: any; output: any };
25
+ };
26
+
27
+ /** Return type for `draftOrderCalculate` mutation. */
28
+ export type DraftOrderCalculatePayload = {
29
+ /** The calculated properties for a draft order. */
30
+ calculatedDraftOrder?: Maybe<CalculatedDraftOrder>;
31
+ };
32
+
33
+ export interface CalculatedDraftOrder {
34
+ /** The available shipping rates for the draft order. Requires a customer with a valid shipping address and at least one line item. */
35
+ availableShippingRates: Array<ShippingRate>;
36
+ }
37
+
38
+ export type ShippingRate = {
39
+ /** Human-readable unique identifier for this shipping rate. */
40
+ handle: Scalars["String"]["output"];
41
+ /** The cost associated with the shipping rate. */
42
+ price: MoneyV2;
43
+ /** The name of the shipping rate. */
44
+ title: Scalars["String"]["output"];
45
+ };
46
+
47
+ export type MoneyV2 = {
48
+ /** Decimal money amount. */
49
+ amount: Scalars["Decimal"]["output"];
50
+ /** Currency of the money. */
51
+ currencyCode: CurrencyCode;
52
+ };
53
+
54
+ export enum CurrencyCode {
55
+ CAD = "CAD",
56
+ Usd = "USD",
57
+ }
@@ -0,0 +1,29 @@
1
+ import { gql } from "../graphql";
2
+
3
+ // Fixme: This is to avoid typescript generation errors
4
+ // because it does not accept generating an empty schema
5
+ // TODO: Remove this once you add any other query
6
+ export const Noop = {
7
+ query: gql`query Noop { app(id: "") { description } }`,
8
+ };
9
+
10
+ export const draftOrderCalculate = {
11
+ query: gql` mutation draftOrderCalculate($input: DraftOrderInput!) {
12
+ calculatedDraftOrder: draftOrderCalculate(input: $input) {
13
+ calculatedDraftOrder {
14
+ availableShippingRates {
15
+ title
16
+ handle
17
+ price {
18
+ amount
19
+ }
20
+ }
21
+ }
22
+ userErrors {
23
+ field
24
+ message
25
+ }
26
+ }
27
+ }
28
+ `,
29
+ };
@@ -0,0 +1,28 @@
1
+ import { getCookies, setCookie } from "./cookies";
2
+
3
+ const CART_COOKIE = "cart";
4
+ const SHOPIFY_PREFIX = "gid://shopify/Cart/";
5
+
6
+ const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000;
7
+
8
+ export const getCartCookie = (headers: Headers): string | null => {
9
+ const cookies = getCookies(headers);
10
+
11
+ if (!cookies[CART_COOKIE]) {
12
+ return null;
13
+ }
14
+
15
+ return decodeURIComponent(`${SHOPIFY_PREFIX}${cookies[CART_COOKIE]}`);
16
+ };
17
+
18
+ export const setCartCookie = (headers: Headers, cartId: string) => {
19
+ setCookie(headers, {
20
+ name: CART_COOKIE,
21
+ value: cartId.replace(SHOPIFY_PREFIX, ""),
22
+ path: "/",
23
+ expires: new Date(Date.now() + ONE_WEEK_MS),
24
+ httpOnly: true,
25
+ secure: true,
26
+ sameSite: "Lax",
27
+ });
28
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Node.js-compatible cookie helpers.
3
+ * Replaces Deno's std/http/cookie.ts.
4
+ */
5
+
6
+ export function getCookies(headers: Headers): Record<string, string> {
7
+ const cookieHeader = headers.get("cookie") || "";
8
+ const cookies: Record<string, string> = {};
9
+ for (const pair of cookieHeader.split(";")) {
10
+ const [key, ...rest] = pair.trim().split("=");
11
+ if (key) {
12
+ cookies[key.trim()] = decodeURIComponent(rest.join("=").trim());
13
+ }
14
+ }
15
+ return cookies;
16
+ }
17
+
18
+ export function setCookie(
19
+ headers: Headers,
20
+ options: {
21
+ name: string;
22
+ value: string;
23
+ path?: string;
24
+ expires?: Date;
25
+ maxAge?: number;
26
+ httpOnly?: boolean;
27
+ secure?: boolean;
28
+ sameSite?: "Strict" | "Lax" | "None";
29
+ }
30
+ ) {
31
+ const parts = [`${options.name}=${encodeURIComponent(options.value)}`];
32
+ if (options.path) parts.push(`Path=${options.path}`);
33
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
34
+ if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
35
+ if (options.httpOnly) parts.push("HttpOnly");
36
+ if (options.secure) parts.push("Secure");
37
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
38
+
39
+ headers.append("Set-Cookie", parts.join("; "));
40
+ }
41
+
42
+ export interface Cookie {
43
+ name: string;
44
+ value: string;
45
+ path?: string;
46
+ expires?: Date;
47
+ maxAge?: number;
48
+ httpOnly?: boolean;
49
+ secure?: boolean;
50
+ sameSite?: "Strict" | "Lax" | "None";
51
+ }
52
+
53
+ export function getSetCookies(headers: Headers): Cookie[] {
54
+ const cookies: Cookie[] = [];
55
+ const setCookieHeaders = headers.getSetCookie?.() ?? [];
56
+ for (const header of setCookieHeaders) {
57
+ const parts = header.split(";").map((p) => p.trim());
58
+ const [nameValue, ...attrs] = parts;
59
+ const [name, ...rest] = nameValue.split("=");
60
+ const value = rest.join("=");
61
+ const cookie: Cookie = { name: name.trim(), value };
62
+ for (const attr of attrs) {
63
+ const [k, v] = attr.split("=");
64
+ const key = k.trim().toLowerCase();
65
+ if (key === "path") cookie.path = v?.trim();
66
+ if (key === "httponly") cookie.httpOnly = true;
67
+ if (key === "secure") cookie.secure = true;
68
+ if (key === "samesite") cookie.sameSite = v?.trim() as any;
69
+ if (key === "max-age") cookie.maxAge = Number(v?.trim());
70
+ }
71
+ cookies.push(cookie);
72
+ }
73
+ return cookies;
74
+ }
75
+
76
+ export function debounce<T extends (...args: any[]) => void>(
77
+ fn: T,
78
+ delay: number
79
+ ): T {
80
+ let timer: ReturnType<typeof setTimeout>;
81
+ return ((...args: any[]) => {
82
+ clearTimeout(timer);
83
+ timer = setTimeout(() => fn(...args), delay);
84
+ }) as T;
85
+ }