@shopify/cli-hydrogen 5.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/commands/hydrogen/build.js +16 -2
  2. package/dist/commands/hydrogen/codegen-unstable.js +13 -24
  3. package/dist/commands/hydrogen/dev.js +45 -39
  4. package/dist/commands/hydrogen/env/list.js +25 -24
  5. package/dist/commands/hydrogen/env/list.test.js +46 -43
  6. package/dist/commands/hydrogen/env/pull.js +53 -25
  7. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  8. package/dist/commands/hydrogen/generate/route.js +31 -132
  9. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  10. package/dist/commands/hydrogen/init.js +46 -127
  11. package/dist/commands/hydrogen/init.test.js +352 -100
  12. package/dist/commands/hydrogen/link.js +70 -69
  13. package/dist/commands/hydrogen/link.test.js +72 -107
  14. package/dist/commands/hydrogen/list.js +22 -12
  15. package/dist/commands/hydrogen/list.test.js +51 -48
  16. package/dist/commands/hydrogen/login.js +31 -0
  17. package/dist/commands/hydrogen/logout.js +21 -0
  18. package/dist/commands/hydrogen/setup/css.js +79 -0
  19. package/dist/commands/hydrogen/setup/markets.js +53 -0
  20. package/dist/commands/hydrogen/setup.js +133 -0
  21. package/dist/commands/hydrogen/shortcut.js +2 -45
  22. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  23. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  24. package/dist/generator-templates/assets/postcss/package.json +10 -0
  25. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  26. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  27. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  28. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  29. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  30. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  31. package/dist/generator-templates/starter/.eslintignore +5 -0
  32. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  33. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  34. package/dist/generator-templates/starter/README.md +40 -0
  35. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  36. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  37. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  38. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  39. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  40. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  41. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  42. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  43. package/dist/generator-templates/starter/app/root.tsx +270 -0
  44. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  45. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  46. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  47. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  48. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  49. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  50. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  51. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  52. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  53. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  54. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  55. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  56. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  57. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  58. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  59. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  60. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  61. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  62. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  63. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  64. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  65. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  66. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  67. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  68. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  69. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  70. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  71. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  72. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  73. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  74. package/dist/generator-templates/starter/app/utils.ts +46 -0
  75. package/dist/generator-templates/starter/package.json +43 -0
  76. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  77. package/dist/generator-templates/starter/remix.config.js +26 -0
  78. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  79. package/dist/generator-templates/starter/server.ts +253 -0
  80. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  81. package/dist/generator-templates/starter/tsconfig.json +22 -0
  82. package/dist/lib/auth.js +123 -0
  83. package/dist/lib/auth.test.js +157 -0
  84. package/dist/lib/build.js +51 -0
  85. package/dist/lib/check-version.js +3 -3
  86. package/dist/lib/check-version.test.js +24 -0
  87. package/dist/lib/codegen.js +26 -17
  88. package/dist/lib/environment-variables.js +68 -0
  89. package/dist/lib/environment-variables.test.js +147 -0
  90. package/dist/lib/file.js +41 -0
  91. package/dist/lib/file.test.js +69 -0
  92. package/dist/lib/flags.js +39 -2
  93. package/dist/lib/format-code.js +26 -0
  94. package/dist/lib/gid.js +12 -0
  95. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  96. package/dist/lib/graphql/admin/client.js +27 -0
  97. package/dist/lib/graphql/admin/client.test.js +51 -0
  98. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  99. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  100. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  101. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  102. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  103. package/dist/lib/graphql/admin/list-environments.js +2 -2
  104. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  105. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  106. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  107. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  108. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  109. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  110. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  111. package/dist/lib/log.js +185 -9
  112. package/dist/lib/log.test.js +92 -0
  113. package/dist/lib/mini-oxygen.js +19 -9
  114. package/dist/lib/missing-routes.js +0 -2
  115. package/dist/lib/onboarding/common.js +456 -0
  116. package/dist/lib/onboarding/index.js +2 -0
  117. package/dist/lib/onboarding/local.js +229 -0
  118. package/dist/lib/onboarding/remote.js +89 -0
  119. package/dist/lib/remix-version-interop.js +5 -5
  120. package/dist/lib/remix-version-interop.test.js +11 -1
  121. package/dist/lib/render-errors.js +13 -11
  122. package/dist/lib/setups/css/assets.js +89 -0
  123. package/dist/lib/setups/css/css-modules.js +22 -0
  124. package/dist/lib/setups/css/index.js +44 -0
  125. package/dist/lib/setups/css/postcss.js +34 -0
  126. package/dist/lib/setups/css/replacers.js +137 -0
  127. package/dist/lib/setups/css/tailwind.js +54 -0
  128. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  129. package/dist/lib/setups/i18n/domains.test.js +25 -0
  130. package/dist/lib/setups/i18n/index.js +46 -0
  131. package/dist/lib/setups/i18n/replacers.js +227 -0
  132. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  133. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  134. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  135. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  136. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  137. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  138. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  139. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  140. package/dist/lib/setups/routes/generate.js +244 -0
  141. package/dist/lib/setups/routes/generate.test.js +313 -0
  142. package/dist/lib/shell.js +52 -5
  143. package/dist/lib/shell.test.js +42 -16
  144. package/dist/lib/shopify-config.js +23 -18
  145. package/dist/lib/shopify-config.test.js +63 -73
  146. package/dist/lib/template-downloader.js +9 -7
  147. package/dist/lib/transpile-ts.js +9 -29
  148. package/dist/virtual-routes/routes/index.jsx +40 -19
  149. package/oclif.manifest.json +710 -1
  150. package/package.json +16 -16
  151. package/dist/commands/hydrogen/build.d.ts +0 -23
  152. package/dist/commands/hydrogen/check.d.ts +0 -15
  153. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  154. package/dist/commands/hydrogen/dev.d.ts +0 -21
  155. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  156. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  157. package/dist/commands/hydrogen/g.d.ts +0 -10
  158. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  159. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  160. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  161. package/dist/commands/hydrogen/init.d.ts +0 -24
  162. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  163. package/dist/commands/hydrogen/link.d.ts +0 -23
  164. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/list.d.ts +0 -21
  166. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  167. package/dist/commands/hydrogen/preview.d.ts +0 -17
  168. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  169. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  171. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  172. package/dist/create-app.d.ts +0 -1
  173. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  174. package/dist/generator-templates/routes/account/login.tsx +0 -103
  175. package/dist/generator-templates/routes/account/register.tsx +0 -103
  176. package/dist/generator-templates/routes/cart.tsx +0 -81
  177. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  178. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  179. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  180. package/dist/generator-templates/routes/index.tsx +0 -40
  181. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  182. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  183. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  184. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  185. package/dist/hooks/init.d.ts +0 -5
  186. package/dist/lib/admin-session.d.ts +0 -6
  187. package/dist/lib/admin-session.js +0 -16
  188. package/dist/lib/admin-session.test.d.ts +0 -1
  189. package/dist/lib/admin-session.test.js +0 -27
  190. package/dist/lib/admin-urls.d.ts +0 -8
  191. package/dist/lib/check-lockfile.d.ts +0 -3
  192. package/dist/lib/check-lockfile.test.d.ts +0 -1
  193. package/dist/lib/check-version.d.ts +0 -16
  194. package/dist/lib/check-version.test.d.ts +0 -1
  195. package/dist/lib/codegen.d.ts +0 -26
  196. package/dist/lib/combined-environment-variables.d.ts +0 -8
  197. package/dist/lib/combined-environment-variables.js +0 -57
  198. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  199. package/dist/lib/combined-environment-variables.test.js +0 -111
  200. package/dist/lib/config.d.ts +0 -20
  201. package/dist/lib/flags.d.ts +0 -27
  202. package/dist/lib/flags.test.d.ts +0 -1
  203. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  204. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  205. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  206. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  207. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  208. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  209. package/dist/lib/graphql.d.ts +0 -21
  210. package/dist/lib/graphql.js +0 -18
  211. package/dist/lib/graphql.test.d.ts +0 -1
  212. package/dist/lib/log.d.ts +0 -6
  213. package/dist/lib/mini-oxygen.d.ts +0 -22
  214. package/dist/lib/missing-routes.d.ts +0 -8
  215. package/dist/lib/missing-routes.test.d.ts +0 -1
  216. package/dist/lib/missing-storefronts.d.ts +0 -5
  217. package/dist/lib/missing-storefronts.js +0 -18
  218. package/dist/lib/process.d.ts +0 -6
  219. package/dist/lib/pull-environment-variables.d.ts +0 -20
  220. package/dist/lib/pull-environment-variables.js +0 -57
  221. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  222. package/dist/lib/pull-environment-variables.test.js +0 -174
  223. package/dist/lib/remix-version-interop.d.ts +0 -11
  224. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  225. package/dist/lib/render-errors.d.ts +0 -16
  226. package/dist/lib/shell.d.ts +0 -11
  227. package/dist/lib/shell.test.d.ts +0 -1
  228. package/dist/lib/shop.d.ts +0 -7
  229. package/dist/lib/shop.js +0 -32
  230. package/dist/lib/shop.test.d.ts +0 -1
  231. package/dist/lib/shop.test.js +0 -78
  232. package/dist/lib/shopify-config.d.ts +0 -35
  233. package/dist/lib/shopify-config.test.d.ts +0 -1
  234. package/dist/lib/string.d.ts +0 -3
  235. package/dist/lib/string.test.d.ts +0 -1
  236. package/dist/lib/template-downloader.d.ts +0 -6
  237. package/dist/lib/transpile-ts.d.ts +0 -16
  238. package/dist/lib/user-errors.d.ts +0 -9
  239. package/dist/lib/user-errors.js +0 -11
  240. package/dist/lib/virtual-routes.d.ts +0 -7
  241. package/dist/lib/virtual-routes.test.d.ts +0 -1
  242. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  243. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -0,0 +1,145 @@
1
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
+ import {defer, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import {Await, useLoaderData, Link} from '@remix-run/react';
4
+ import {Suspense} from 'react';
5
+ import {Image, Money} from '@shopify/hydrogen';
6
+ import type {
7
+ FeaturedCollectionFragment,
8
+ RecommendedProductsQuery,
9
+ } from 'storefrontapi.generated';
10
+
11
+ export const meta: V2_MetaFunction = () => {
12
+ return [{title: 'Hydrogen | Home'}];
13
+ };
14
+
15
+ export async function loader({context}: LoaderArgs) {
16
+ const {storefront} = context;
17
+ const {collections} = await storefront.query(FEATURED_COLLECTION_QUERY);
18
+ const featuredCollection = collections.nodes[0];
19
+ const recommendedProducts = storefront.query(RECOMMENDED_PRODUCTS_QUERY);
20
+
21
+ return defer({featuredCollection, recommendedProducts});
22
+ }
23
+
24
+ export default function Homepage() {
25
+ const data = useLoaderData<typeof loader>();
26
+ return (
27
+ <div className="home">
28
+ <FeaturedCollection collection={data.featuredCollection} />
29
+ <RecommendedProducts products={data.recommendedProducts} />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function FeaturedCollection({
35
+ collection,
36
+ }: {
37
+ collection: FeaturedCollectionFragment;
38
+ }) {
39
+ const image = collection.image;
40
+ return (
41
+ <Link
42
+ className="featured-collection"
43
+ to={`/collections/${collection.handle}`}
44
+ >
45
+ {image && (
46
+ <div className="featured-collection-image">
47
+ <Image data={image} sizes="100vw" />
48
+ </div>
49
+ )}
50
+ <h1>{collection.title}</h1>
51
+ </Link>
52
+ );
53
+ }
54
+
55
+ function RecommendedProducts({
56
+ products,
57
+ }: {
58
+ products: Promise<RecommendedProductsQuery>;
59
+ }) {
60
+ return (
61
+ <div className="recommended-products">
62
+ <h2>Recommended Products</h2>
63
+ <Suspense fallback={<div>Loading...</div>}>
64
+ <Await resolve={products}>
65
+ {({products}) => (
66
+ <div className="recommended-products-grid">
67
+ {products.nodes.map((product) => (
68
+ <Link
69
+ key={product.id}
70
+ className="recommended-product"
71
+ to={`/products/${product.handle}`}
72
+ >
73
+ <Image
74
+ data={product.images.nodes[0]}
75
+ aspectRatio="1/1"
76
+ sizes="(min-width: 45em) 20vw, 50vw"
77
+ />
78
+ <h4>{product.title}</h4>
79
+ <small>
80
+ <Money data={product.priceRange.minVariantPrice} />
81
+ </small>
82
+ </Link>
83
+ ))}
84
+ </div>
85
+ )}
86
+ </Await>
87
+ </Suspense>
88
+ <br />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ const FEATURED_COLLECTION_QUERY = `#graphql
94
+ fragment FeaturedCollection on Collection {
95
+ id
96
+ title
97
+ image {
98
+ id
99
+ url
100
+ altText
101
+ width
102
+ height
103
+ }
104
+ handle
105
+ }
106
+ query FeaturedCollection($country: CountryCode, $language: LanguageCode)
107
+ @inContext(country: $country, language: $language) {
108
+ collections(first: 1, sortKey: UPDATED_AT, reverse: true) {
109
+ nodes {
110
+ ...FeaturedCollection
111
+ }
112
+ }
113
+ }
114
+ ` as const;
115
+
116
+ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
117
+ fragment RecommendedProduct on Product {
118
+ id
119
+ title
120
+ handle
121
+ priceRange {
122
+ minVariantPrice {
123
+ amount
124
+ currencyCode
125
+ }
126
+ }
127
+ images(first: 1) {
128
+ nodes {
129
+ id
130
+ url
131
+ altText
132
+ width
133
+ height
134
+ }
135
+ }
136
+ }
137
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
138
+ @inContext(country: $country, language: $language) {
139
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
140
+ nodes {
141
+ ...RecommendedProduct
142
+ }
143
+ }
144
+ }
145
+ ` as const;
@@ -0,0 +1,9 @@
1
+ import type {LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {redirect} from '@shopify/remix-oxygen';
3
+
4
+ export async function loader({context}: LoaderArgs) {
5
+ if (await context.session.get('customerAccessToken')) {
6
+ return redirect('/account');
7
+ }
8
+ return redirect('/account/login');
9
+ }
@@ -0,0 +1,563 @@
1
+ import type {MailingAddressInput} from '@shopify/hydrogen/storefront-api-types';
2
+ import type {AddressFragment, CustomerFragment} from 'storefrontapi.generated';
3
+ import {
4
+ json,
5
+ redirect,
6
+ type ActionArgs,
7
+ type LoaderArgs,
8
+ type V2_MetaFunction,
9
+ } from '@shopify/remix-oxygen';
10
+ import {
11
+ Form,
12
+ useActionData,
13
+ useNavigation,
14
+ useOutletContext,
15
+ } from '@remix-run/react';
16
+
17
+ export type ActionResponse = {
18
+ addressId?: string | null;
19
+ createdAddress?: AddressFragment;
20
+ defaultAddress?: string | null;
21
+ deletedAddress?: string | null;
22
+ error: Record<AddressFragment['id'], string> | null;
23
+ updatedAddress?: AddressFragment;
24
+ };
25
+
26
+ export const meta: V2_MetaFunction = () => {
27
+ return [{title: 'Addresses'}];
28
+ };
29
+
30
+ export async function loader({context}: LoaderArgs) {
31
+ const {session} = context;
32
+ const customerAccessToken = await session.get('customerAccessToken');
33
+ if (!customerAccessToken) {
34
+ return redirect('/account/login');
35
+ }
36
+ return json({});
37
+ }
38
+
39
+ export async function action({request, context}: ActionArgs) {
40
+ const {storefront, session} = context;
41
+
42
+ try {
43
+ const form = await request.formData();
44
+
45
+ const addressId = form.has('addressId')
46
+ ? String(form.get('addressId'))
47
+ : null;
48
+ if (!addressId) {
49
+ throw new Error('You must provide an address id.');
50
+ }
51
+
52
+ const customerAccessToken = await session.get('customerAccessToken');
53
+ if (!customerAccessToken) {
54
+ return json({error: {[addressId]: 'Unauthorized'}}, {status: 401});
55
+ }
56
+ const {accessToken} = customerAccessToken;
57
+
58
+ const defaultAddress = form.has('defaultAddress')
59
+ ? String(form.get('defaultAddress')) === 'on'
60
+ : null;
61
+ const address: MailingAddressInput = {};
62
+ const keys: (keyof MailingAddressInput)[] = [
63
+ 'address1',
64
+ 'address2',
65
+ 'city',
66
+ 'company',
67
+ 'country',
68
+ 'firstName',
69
+ 'lastName',
70
+ 'phone',
71
+ 'province',
72
+ 'zip',
73
+ ];
74
+
75
+ for (const key of keys) {
76
+ const value = form.get(key);
77
+ if (typeof value === 'string') {
78
+ address[key] = value;
79
+ }
80
+ }
81
+
82
+ switch (request.method) {
83
+ case 'POST': {
84
+ // handle new address creation
85
+ try {
86
+ const {customerAddressCreate} = await storefront.mutate(
87
+ CREATE_ADDRESS_MUTATION,
88
+ {
89
+ variables: {customerAccessToken: accessToken, address},
90
+ },
91
+ );
92
+
93
+ if (customerAddressCreate?.customerUserErrors?.length) {
94
+ const error = customerAddressCreate.customerUserErrors[0];
95
+ throw new Error(error.message);
96
+ }
97
+
98
+ const createdAddress = customerAddressCreate?.customerAddress;
99
+ if (!createdAddress?.id) {
100
+ throw new Error(
101
+ 'Expected customer address to be created, but the id is missing',
102
+ );
103
+ }
104
+
105
+ if (defaultAddress) {
106
+ const createdAddressId = decodeURIComponent(createdAddress.id);
107
+ const {customerDefaultAddressUpdate} = await storefront.mutate(
108
+ UPDATE_DEFAULT_ADDRESS_MUTATION,
109
+ {
110
+ variables: {
111
+ customerAccessToken: accessToken,
112
+ addressId: createdAddressId,
113
+ },
114
+ },
115
+ );
116
+
117
+ if (customerDefaultAddressUpdate?.customerUserErrors?.length) {
118
+ const error = customerDefaultAddressUpdate.customerUserErrors[0];
119
+ throw new Error(error.message);
120
+ }
121
+ }
122
+
123
+ return json({error: null, createdAddress, defaultAddress});
124
+ } catch (error: unknown) {
125
+ if (error instanceof Error) {
126
+ return json({error: {[addressId]: error.message}}, {status: 400});
127
+ }
128
+ return json({error: {[addressId]: error}}, {status: 400});
129
+ }
130
+ }
131
+
132
+ case 'PUT': {
133
+ // handle address updates
134
+ try {
135
+ const {customerAddressUpdate} = await storefront.mutate(
136
+ UPDATE_ADDRESS_MUTATION,
137
+ {
138
+ variables: {
139
+ address,
140
+ customerAccessToken: accessToken,
141
+ id: decodeURIComponent(addressId),
142
+ },
143
+ },
144
+ );
145
+
146
+ const updatedAddress = customerAddressUpdate?.customerAddress;
147
+
148
+ if (customerAddressUpdate?.customerUserErrors?.length) {
149
+ const error = customerAddressUpdate.customerUserErrors[0];
150
+ throw new Error(error.message);
151
+ }
152
+
153
+ if (defaultAddress) {
154
+ const {customerDefaultAddressUpdate} = await storefront.mutate(
155
+ UPDATE_DEFAULT_ADDRESS_MUTATION,
156
+ {
157
+ variables: {
158
+ customerAccessToken: accessToken,
159
+ addressId: decodeURIComponent(addressId),
160
+ },
161
+ },
162
+ );
163
+
164
+ if (customerDefaultAddressUpdate?.customerUserErrors?.length) {
165
+ const error = customerDefaultAddressUpdate.customerUserErrors[0];
166
+ throw new Error(error.message);
167
+ }
168
+ }
169
+
170
+ return json({error: null, updatedAddress, defaultAddress});
171
+ } catch (error: unknown) {
172
+ if (error instanceof Error) {
173
+ return json({error: {[addressId]: error.message}}, {status: 400});
174
+ }
175
+ return json({error: {[addressId]: error}}, {status: 400});
176
+ }
177
+ }
178
+
179
+ case 'DELETE': {
180
+ // handles address deletion
181
+ try {
182
+ const {customerAddressDelete} = await storefront.mutate(
183
+ DELETE_ADDRESS_MUTATION,
184
+ {
185
+ variables: {customerAccessToken: accessToken, id: addressId},
186
+ },
187
+ );
188
+
189
+ if (customerAddressDelete?.customerUserErrors?.length) {
190
+ const error = customerAddressDelete.customerUserErrors[0];
191
+ throw new Error(error.message);
192
+ }
193
+ return json({error: null, deletedAddress: addressId});
194
+ } catch (error: unknown) {
195
+ if (error instanceof Error) {
196
+ return json({error: {[addressId]: error.message}}, {status: 400});
197
+ }
198
+ return json({error: {[addressId]: error}}, {status: 400});
199
+ }
200
+ }
201
+
202
+ default: {
203
+ return json(
204
+ {error: {[addressId]: 'Method not allowed'}},
205
+ {status: 405},
206
+ );
207
+ }
208
+ }
209
+ } catch (error: unknown) {
210
+ if (error instanceof Error) {
211
+ return json({error: error.message}, {status: 400});
212
+ }
213
+ return json({error}, {status: 400});
214
+ }
215
+ }
216
+
217
+ export default function Addresses() {
218
+ const {customer} = useOutletContext<{customer: CustomerFragment}>();
219
+ const {defaultAddress, addresses} = customer;
220
+
221
+ return (
222
+ <div className="account-addresses">
223
+ <h2>Addresses</h2>
224
+ <br />
225
+ {!addresses.nodes.length ? (
226
+ <p>You have no addresses saved.</p>
227
+ ) : (
228
+ <div>
229
+ <div>
230
+ <legend>Create address</legend>
231
+ <NewAddressForm />
232
+ </div>
233
+ <br />
234
+ <hr />
235
+ <br />
236
+ <ExistingAddresses
237
+ addresses={addresses}
238
+ defaultAddress={defaultAddress}
239
+ />
240
+ </div>
241
+ )}
242
+ </div>
243
+ );
244
+ }
245
+
246
+ function NewAddressForm() {
247
+ const newAddress = {
248
+ address1: '',
249
+ address2: '',
250
+ city: '',
251
+ company: '',
252
+ country: '',
253
+ firstName: '',
254
+ id: 'new',
255
+ lastName: '',
256
+ phone: '',
257
+ province: '',
258
+ zip: '',
259
+ } as AddressFragment;
260
+
261
+ return (
262
+ <AddressForm address={newAddress} defaultAddress={null}>
263
+ {({stateForMethod}) => (
264
+ <div>
265
+ <button
266
+ disabled={stateForMethod('POST') !== 'idle'}
267
+ formMethod="POST"
268
+ type="submit"
269
+ >
270
+ {stateForMethod('POST') !== 'idle' ? 'Creating' : 'Create'}
271
+ </button>
272
+ </div>
273
+ )}
274
+ </AddressForm>
275
+ );
276
+ }
277
+
278
+ function ExistingAddresses({
279
+ addresses,
280
+ defaultAddress,
281
+ }: Pick<CustomerFragment, 'addresses' | 'defaultAddress'>) {
282
+ return (
283
+ <div>
284
+ <legend>Existing addresses</legend>
285
+ {addresses.nodes.map((address) => (
286
+ <AddressForm
287
+ key={address.id}
288
+ address={address}
289
+ defaultAddress={defaultAddress}
290
+ >
291
+ {({stateForMethod}) => (
292
+ <div>
293
+ <button
294
+ disabled={stateForMethod('PUT') !== 'idle'}
295
+ formMethod="PUT"
296
+ type="submit"
297
+ >
298
+ {stateForMethod('PUT') !== 'idle' ? 'Saving' : 'Save'}
299
+ </button>
300
+ <button
301
+ disabled={stateForMethod('DELETE') !== 'idle'}
302
+ formMethod="DELETE"
303
+ type="submit"
304
+ >
305
+ {stateForMethod('DELETE') !== 'idle' ? 'Deleting' : 'Delete'}
306
+ </button>
307
+ </div>
308
+ )}
309
+ </AddressForm>
310
+ ))}
311
+ </div>
312
+ );
313
+ }
314
+
315
+ export function AddressForm({
316
+ address,
317
+ defaultAddress,
318
+ children,
319
+ }: {
320
+ children: (props: {
321
+ stateForMethod: (
322
+ method: 'PUT' | 'POST' | 'DELETE',
323
+ ) => ReturnType<typeof useNavigation>['state'];
324
+ }) => React.ReactNode;
325
+ defaultAddress: CustomerFragment['defaultAddress'];
326
+ address: AddressFragment;
327
+ }) {
328
+ const {state, formMethod} = useNavigation();
329
+ const action = useActionData<ActionResponse>();
330
+ const error = action?.error?.[address.id];
331
+ const isDefaultAddress = defaultAddress?.id === address.id;
332
+ return (
333
+ <Form id={address.id}>
334
+ <fieldset>
335
+ <input type="hidden" name="addressId" defaultValue={address.id} />
336
+ <label htmlFor="firstName">First name*</label>
337
+ <input
338
+ aria-label="First name"
339
+ autoComplete="given-name"
340
+ defaultValue={address?.firstName ?? ''}
341
+ id="firstName"
342
+ name="firstName"
343
+ placeholder="First name"
344
+ required
345
+ type="text"
346
+ />
347
+ <label htmlFor="lastName">Last name*</label>
348
+ <input
349
+ aria-label="Last name"
350
+ autoComplete="family-name"
351
+ defaultValue={address?.lastName ?? ''}
352
+ id="lastName"
353
+ name="lastName"
354
+ placeholder="Last name"
355
+ required
356
+ type="text"
357
+ />
358
+ <label htmlFor="company">Company</label>
359
+ <input
360
+ aria-label="Company"
361
+ autoComplete="organization"
362
+ defaultValue={address?.company ?? ''}
363
+ id="company"
364
+ name="company"
365
+ placeholder="Company"
366
+ type="text"
367
+ />
368
+ <label htmlFor="address1">Address line*</label>
369
+ <input
370
+ aria-label="Address line 1"
371
+ autoComplete="address-line1"
372
+ defaultValue={address?.address1 ?? ''}
373
+ id="address1"
374
+ name="address1"
375
+ placeholder="Address line 1*"
376
+ required
377
+ type="text"
378
+ />
379
+ <label htmlFor="address2">Address line 2</label>
380
+ <input
381
+ aria-label="Address line 2"
382
+ autoComplete="address-line2"
383
+ defaultValue={address?.address2 ?? ''}
384
+ id="address2"
385
+ name="address2"
386
+ placeholder="Address line 2"
387
+ type="text"
388
+ />
389
+ <label htmlFor="city">City*</label>
390
+ <input
391
+ aria-label="City"
392
+ autoComplete="address-level2"
393
+ defaultValue={address?.city ?? ''}
394
+ id="city"
395
+ name="city"
396
+ placeholder="City"
397
+ required
398
+ type="text"
399
+ />
400
+ <label htmlFor="province">State / Province*</label>
401
+ <input
402
+ aria-label="State"
403
+ autoComplete="address-level1"
404
+ defaultValue={address?.province ?? ''}
405
+ id="province"
406
+ name="province"
407
+ placeholder="State / Province"
408
+ required
409
+ type="text"
410
+ />
411
+ <label htmlFor="zip">Zip / Postal Code*</label>
412
+ <input
413
+ aria-label="Zip"
414
+ autoComplete="postal-code"
415
+ defaultValue={address?.zip ?? ''}
416
+ id="zip"
417
+ name="zip"
418
+ placeholder="Zip / Postal Code"
419
+ required
420
+ type="text"
421
+ />
422
+ <label htmlFor="country">Country*</label>
423
+ <input
424
+ aria-label="Country"
425
+ autoComplete="country-name"
426
+ defaultValue={address?.country ?? ''}
427
+ id="country"
428
+ name="country"
429
+ placeholder="Country"
430
+ required
431
+ type="text"
432
+ />
433
+ <label htmlFor="phone">Phone</label>
434
+ <input
435
+ aria-label="Phone"
436
+ autoComplete="tel"
437
+ defaultValue={address?.phone ?? ''}
438
+ id="phone"
439
+ name="phone"
440
+ placeholder="+16135551111"
441
+ pattern="^\+?[1-9]\d{3,14}$"
442
+ type="tel"
443
+ />
444
+ <div>
445
+ <input
446
+ defaultChecked={isDefaultAddress}
447
+ id="defaultAddress"
448
+ name="defaultAddress"
449
+ type="checkbox"
450
+ />
451
+ <label htmlFor="defaultAddress">Set as default address</label>
452
+ </div>
453
+ {error ? (
454
+ <p>
455
+ <mark>
456
+ <small>{error}</small>
457
+ </mark>
458
+ </p>
459
+ ) : (
460
+ <br />
461
+ )}
462
+ {children({
463
+ stateForMethod: (method) => (formMethod === method ? state : 'idle'),
464
+ })}
465
+ </fieldset>
466
+ </Form>
467
+ );
468
+ }
469
+
470
+ // NOTE: https://shopify.dev/docs/api/storefront/2023-04/mutations/customeraddressupdate
471
+ const UPDATE_ADDRESS_MUTATION = `#graphql
472
+ mutation customerAddressUpdate(
473
+ $address: MailingAddressInput!
474
+ $customerAccessToken: String!
475
+ $id: ID!
476
+ $country: CountryCode
477
+ $language: LanguageCode
478
+ ) @inContext(country: $country, language: $language) {
479
+ customerAddressUpdate(
480
+ address: $address
481
+ customerAccessToken: $customerAccessToken
482
+ id: $id
483
+ ) {
484
+ customerAddress {
485
+ id
486
+ }
487
+ customerUserErrors {
488
+ code
489
+ field
490
+ message
491
+ }
492
+ }
493
+ }
494
+ ` as const;
495
+
496
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerAddressDelete
497
+ const DELETE_ADDRESS_MUTATION = `#graphql
498
+ mutation customerAddressDelete(
499
+ $customerAccessToken: String!,
500
+ $id: ID!,
501
+ $country: CountryCode,
502
+ $language: LanguageCode
503
+ ) @inContext(country: $country, language: $language) {
504
+ customerAddressDelete(customerAccessToken: $customerAccessToken, id: $id) {
505
+ customerUserErrors {
506
+ code
507
+ field
508
+ message
509
+ }
510
+ deletedCustomerAddressId
511
+ }
512
+ }
513
+ ` as const;
514
+
515
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerdefaultaddressupdate
516
+ const UPDATE_DEFAULT_ADDRESS_MUTATION = `#graphql
517
+ mutation customerDefaultAddressUpdate(
518
+ $addressId: ID!
519
+ $customerAccessToken: String!
520
+ $country: CountryCode
521
+ $language: LanguageCode
522
+ ) @inContext(country: $country, language: $language) {
523
+ customerDefaultAddressUpdate(
524
+ addressId: $addressId
525
+ customerAccessToken: $customerAccessToken
526
+ ) {
527
+ customer {
528
+ defaultAddress {
529
+ id
530
+ }
531
+ }
532
+ customerUserErrors {
533
+ code
534
+ field
535
+ message
536
+ }
537
+ }
538
+ }
539
+ ` as const;
540
+
541
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customeraddresscreate
542
+ const CREATE_ADDRESS_MUTATION = `#graphql
543
+ mutation customerAddressCreate(
544
+ $address: MailingAddressInput!
545
+ $customerAccessToken: String!
546
+ $country: CountryCode
547
+ $language: LanguageCode
548
+ ) @inContext(country: $country, language: $language) {
549
+ customerAddressCreate(
550
+ address: $address
551
+ customerAccessToken: $customerAccessToken
552
+ ) {
553
+ customerAddress {
554
+ id
555
+ }
556
+ customerUserErrors {
557
+ code
558
+ field
559
+ message
560
+ }
561
+ }
562
+ }
563
+ ` as const;