@shopify/cli-hydrogen 3.26.0 → 4.0.0-alpha.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 (249) hide show
  1. package/dist/commands/hydrogen/build.js +89 -0
  2. package/dist/commands/hydrogen/dev.js +116 -0
  3. package/dist/commands/hydrogen/init.js +42 -0
  4. package/dist/commands/hydrogen/preview.js +34 -0
  5. package/dist/hooks/init.js +21 -0
  6. package/dist/templates/demo-store/.editorconfig +8 -0
  7. package/dist/templates/demo-store/.eslintignore +4 -0
  8. package/dist/templates/demo-store/.eslintrc.js +16 -0
  9. package/dist/templates/demo-store/.graphqlrc.yml +1 -0
  10. package/dist/templates/demo-store/.prettierignore +2 -0
  11. package/dist/templates/demo-store/.turbo/turbo-build.log +13 -0
  12. package/dist/templates/demo-store/app/components/AccountAddressBook.tsx +97 -0
  13. package/dist/templates/demo-store/app/components/AccountDetails.tsx +41 -0
  14. package/dist/templates/demo-store/app/components/AddToCartButton.tsx +42 -0
  15. package/dist/templates/demo-store/app/components/Breadcrumbs.tsx +36 -0
  16. package/dist/templates/demo-store/app/components/Button.tsx +56 -0
  17. package/dist/templates/demo-store/app/components/Cart.tsx +431 -0
  18. package/dist/templates/demo-store/app/components/CartLoading.tsx +50 -0
  19. package/dist/templates/demo-store/app/components/CountrySelector.tsx +180 -0
  20. package/dist/templates/demo-store/app/components/Drawer.tsx +115 -0
  21. package/dist/templates/demo-store/app/components/FeaturedCollections.tsx +54 -0
  22. package/dist/templates/demo-store/app/components/FeaturedProducts.tsx +116 -0
  23. package/dist/templates/demo-store/app/components/FeaturedSection.tsx +39 -0
  24. package/dist/templates/demo-store/app/components/GenericError.tsx +58 -0
  25. package/dist/templates/demo-store/app/components/Grid.tsx +44 -0
  26. package/dist/templates/demo-store/app/components/Hero.tsx +136 -0
  27. package/dist/templates/demo-store/app/components/Icon.tsx +253 -0
  28. package/dist/templates/demo-store/app/components/Input.tsx +24 -0
  29. package/dist/templates/demo-store/app/components/Layout.tsx +492 -0
  30. package/dist/templates/demo-store/app/components/Link.tsx +46 -0
  31. package/dist/templates/demo-store/app/components/Modal.tsx +46 -0
  32. package/dist/templates/demo-store/app/components/NotFound.tsx +22 -0
  33. package/dist/templates/demo-store/app/components/OrderCard.tsx +85 -0
  34. package/dist/templates/demo-store/app/components/Pagination.tsx +277 -0
  35. package/dist/templates/demo-store/app/components/ProductCard.tsx +146 -0
  36. package/dist/templates/demo-store/app/components/ProductGallery.tsx +114 -0
  37. package/dist/templates/demo-store/app/components/ProductGrid.tsx +93 -0
  38. package/dist/templates/demo-store/app/components/ProductSwimlane.tsx +30 -0
  39. package/dist/templates/demo-store/app/components/Skeleton.tsx +24 -0
  40. package/dist/templates/demo-store/app/components/SortFilter.tsx +411 -0
  41. package/dist/templates/demo-store/app/components/Text.tsx +192 -0
  42. package/dist/templates/demo-store/app/components/index.ts +28 -0
  43. package/dist/templates/demo-store/app/data/countries.ts +194 -0
  44. package/dist/templates/demo-store/app/data/index.ts +1037 -0
  45. package/dist/templates/demo-store/app/entry.client.tsx +4 -0
  46. package/dist/templates/demo-store/app/entry.server.tsx +26 -0
  47. package/dist/templates/demo-store/app/hooks/useCartFetchers.tsx +14 -0
  48. package/dist/templates/demo-store/app/hooks/useIsHydrated.tsx +12 -0
  49. package/dist/templates/demo-store/app/lib/const.ts +10 -0
  50. package/dist/templates/demo-store/app/lib/placeholders.ts +242 -0
  51. package/dist/templates/demo-store/app/lib/seo/common.tsx +324 -0
  52. package/dist/templates/demo-store/app/lib/seo/debugger.tsx +175 -0
  53. package/dist/templates/demo-store/app/lib/seo/image.tsx +32 -0
  54. package/dist/templates/demo-store/app/lib/seo/index.ts +4 -0
  55. package/dist/templates/demo-store/app/lib/seo/seo.tsx +24 -0
  56. package/dist/templates/demo-store/app/lib/seo/types.ts +70 -0
  57. package/dist/templates/demo-store/app/lib/session.server.ts +57 -0
  58. package/dist/templates/demo-store/app/lib/type.ts +21 -0
  59. package/dist/templates/demo-store/app/lib/utils.ts +310 -0
  60. package/dist/templates/demo-store/app/root.tsx +282 -0
  61. package/dist/templates/demo-store/app/routes/$.tsx +7 -0
  62. package/dist/templates/demo-store/app/routes/$lang/$.tsx +1 -0
  63. package/dist/templates/demo-store/app/routes/$lang/[robots.txt].tsx +1 -0
  64. package/dist/templates/demo-store/app/routes/$lang/[sitemap.xml].tsx +1 -0
  65. package/dist/templates/demo-store/app/routes/$lang/account/__private/address/$id.tsx +1 -0
  66. package/dist/templates/demo-store/app/routes/$lang/account/__private/edit.tsx +1 -0
  67. package/dist/templates/demo-store/app/routes/$lang/account/__private/logout.ts +1 -0
  68. package/dist/templates/demo-store/app/routes/$lang/account/__private/orders.$id.tsx +1 -0
  69. package/dist/templates/demo-store/app/routes/$lang/account/__public/activate.$id.$activationToken.tsx +6 -0
  70. package/dist/templates/demo-store/app/routes/$lang/account/__public/login.tsx +7 -0
  71. package/dist/templates/demo-store/app/routes/$lang/account/__public/recover.tsx +1 -0
  72. package/dist/templates/demo-store/app/routes/$lang/account/__public/register.tsx +6 -0
  73. package/dist/templates/demo-store/app/routes/$lang/account/__public/reset.$id.$resetToken.tsx +5 -0
  74. package/dist/templates/demo-store/app/routes/$lang/account.tsx +1 -0
  75. package/dist/templates/demo-store/app/routes/$lang/api/countries.tsx +1 -0
  76. package/dist/templates/demo-store/app/routes/$lang/api/products.tsx +1 -0
  77. package/dist/templates/demo-store/app/routes/$lang/cart.tsx +1 -0
  78. package/dist/templates/demo-store/app/routes/$lang/collections/$collectionHandle.tsx +6 -0
  79. package/dist/templates/demo-store/app/routes/$lang/collections/all.tsx +1 -0
  80. package/dist/templates/demo-store/app/routes/$lang/collections/index.tsx +1 -0
  81. package/dist/templates/demo-store/app/routes/$lang/featured-products.tsx +1 -0
  82. package/dist/templates/demo-store/app/routes/$lang/index.tsx +7 -0
  83. package/dist/templates/demo-store/app/routes/$lang/journal/$journalHandle.tsx +7 -0
  84. package/dist/templates/demo-store/app/routes/$lang/journal/index.tsx +1 -0
  85. package/dist/templates/demo-store/app/routes/$lang/og-image.tsx +1 -0
  86. package/dist/templates/demo-store/app/routes/$lang/pages/$pageHandle.tsx +1 -0
  87. package/dist/templates/demo-store/app/routes/$lang/policies/$policyHandle.tsx +1 -0
  88. package/dist/templates/demo-store/app/routes/$lang/policies/index.tsx +1 -0
  89. package/dist/templates/demo-store/app/routes/$lang/products/$productHandle.tsx +6 -0
  90. package/dist/templates/demo-store/app/routes/$lang/products/index.tsx +1 -0
  91. package/dist/templates/demo-store/app/routes/$lang/search.tsx +6 -0
  92. package/dist/templates/demo-store/app/routes/[robots.txt].tsx +40 -0
  93. package/dist/templates/demo-store/app/routes/[sitemap.xml].tsx +198 -0
  94. package/dist/templates/demo-store/app/routes/account/__private/address/$id.tsx +320 -0
  95. package/dist/templates/demo-store/app/routes/account/__private/edit.tsx +273 -0
  96. package/dist/templates/demo-store/app/routes/account/__private/logout.ts +29 -0
  97. package/dist/templates/demo-store/app/routes/account/__private/orders.$id.tsx +324 -0
  98. package/dist/templates/demo-store/app/routes/account/__public/activate.$id.$activationToken.tsx +218 -0
  99. package/dist/templates/demo-store/app/routes/account/__public/login.tsx +197 -0
  100. package/dist/templates/demo-store/app/routes/account/__public/recover.tsx +144 -0
  101. package/dist/templates/demo-store/app/routes/account/__public/register.tsx +184 -0
  102. package/dist/templates/demo-store/app/routes/account/__public/reset.$id.$resetToken.tsx +214 -0
  103. package/dist/templates/demo-store/app/routes/account.tsx +191 -0
  104. package/dist/templates/demo-store/app/routes/api/countries.tsx +22 -0
  105. package/dist/templates/demo-store/app/routes/api/products.tsx +116 -0
  106. package/dist/templates/demo-store/app/routes/cart.tsx +498 -0
  107. package/dist/templates/demo-store/app/routes/collections/$collectionHandle.tsx +308 -0
  108. package/dist/templates/demo-store/app/routes/collections/all.tsx +5 -0
  109. package/dist/templates/demo-store/app/routes/collections/index.tsx +195 -0
  110. package/dist/templates/demo-store/app/routes/discounts.$code.tsx +60 -0
  111. package/dist/templates/demo-store/app/routes/featured-products.tsx +58 -0
  112. package/dist/templates/demo-store/app/routes/index.tsx +254 -0
  113. package/dist/templates/demo-store/app/routes/journal/$journalHandle.tsx +147 -0
  114. package/dist/templates/demo-store/app/routes/journal/index.tsx +150 -0
  115. package/dist/templates/demo-store/app/routes/og-image.tsx +19 -0
  116. package/dist/templates/demo-store/app/routes/pages/$pageHandle.tsx +82 -0
  117. package/dist/templates/demo-store/app/routes/policies/$policyHandle.tsx +117 -0
  118. package/dist/templates/demo-store/app/routes/policies/index.tsx +104 -0
  119. package/dist/templates/demo-store/app/routes/products/$productHandle.tsx +561 -0
  120. package/dist/templates/demo-store/app/routes/products/index.tsx +155 -0
  121. package/dist/templates/demo-store/app/routes/search.tsx +205 -0
  122. package/dist/templates/demo-store/app/styles/custom-font.css +13 -0
  123. package/dist/templates/demo-store/package-lock.json +25515 -0
  124. package/dist/templates/demo-store/package.json +67 -0
  125. package/dist/templates/demo-store/playwright.config.ts +109 -0
  126. package/dist/templates/demo-store/postcss.config.js +10 -0
  127. package/dist/templates/demo-store/public/favicon.svg +28 -0
  128. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-Text.woff2 +0 -0
  129. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-TextItalic.woff2 +0 -0
  130. package/dist/templates/demo-store/remix.config.js +12 -0
  131. package/dist/templates/demo-store/remix.env.d.ts +34 -0
  132. package/dist/templates/demo-store/remix.init/index.ts +15 -0
  133. package/dist/templates/demo-store/remix.init/package.json +7 -0
  134. package/dist/templates/demo-store/server.ts +87 -0
  135. package/dist/templates/demo-store/styles/app.css +182 -0
  136. package/dist/templates/demo-store/tailwind.config.js +70 -0
  137. package/dist/templates/demo-store/tests/cart.test.ts +70 -0
  138. package/dist/templates/demo-store/tests/seo.test.ts +36 -0
  139. package/dist/templates/demo-store/tests/utils.ts +100 -0
  140. package/dist/templates/demo-store/tsconfig.json +26 -0
  141. package/dist/templates/hello-world/.eslintignore +4 -0
  142. package/dist/templates/hello-world/.eslintrc.js +6 -0
  143. package/dist/templates/hello-world/.graphqlrc.yml +1 -0
  144. package/dist/templates/hello-world/.turbo/turbo-build.log +9 -0
  145. package/dist/templates/hello-world/README.md +20 -0
  146. package/dist/templates/hello-world/app/components/Layout.tsx +15 -0
  147. package/dist/templates/hello-world/app/components/index.ts +1 -0
  148. package/dist/templates/hello-world/app/entry.client.tsx +4 -0
  149. package/dist/templates/hello-world/app/entry.server.tsx +21 -0
  150. package/dist/templates/hello-world/app/root.tsx +212 -0
  151. package/dist/templates/hello-world/app/routes/index.tsx +7 -0
  152. package/dist/templates/hello-world/app/styles/app.css +38 -0
  153. package/dist/templates/hello-world/package-lock.json +27641 -0
  154. package/dist/templates/hello-world/package.json +41 -0
  155. package/dist/templates/hello-world/public/favicon.svg +28 -0
  156. package/dist/templates/hello-world/remix.env.d.ts +29 -0
  157. package/dist/templates/hello-world/server.ts +127 -0
  158. package/dist/templates/hello-world/tsconfig.json +25 -0
  159. package/dist/utils/config.js +81 -0
  160. package/dist/utils/flags.js +15 -0
  161. package/dist/utils/log.js +20 -0
  162. package/dist/utils/mini-oxygen.js +70 -0
  163. package/package.json +27 -64
  164. package/tmp-create-app.mjs +29 -0
  165. package/LICENSE +0 -8
  166. package/README.md +0 -61
  167. package/dist/cli/commands/hydrogen/add/eslint.d.ts +0 -11
  168. package/dist/cli/commands/hydrogen/add/eslint.js +0 -26
  169. package/dist/cli/commands/hydrogen/add/eslint.js.map +0 -1
  170. package/dist/cli/commands/hydrogen/add/tailwind.d.ts +0 -11
  171. package/dist/cli/commands/hydrogen/add/tailwind.js +0 -26
  172. package/dist/cli/commands/hydrogen/add/tailwind.js.map +0 -1
  173. package/dist/cli/commands/hydrogen/build.d.ts +0 -14
  174. package/dist/cli/commands/hydrogen/build.js +0 -49
  175. package/dist/cli/commands/hydrogen/build.js.map +0 -1
  176. package/dist/cli/commands/hydrogen/deploy.d.ts +0 -19
  177. package/dist/cli/commands/hydrogen/deploy.js +0 -58
  178. package/dist/cli/commands/hydrogen/deploy.js.map +0 -1
  179. package/dist/cli/commands/hydrogen/dev.d.ts +0 -13
  180. package/dist/cli/commands/hydrogen/dev.js +0 -31
  181. package/dist/cli/commands/hydrogen/dev.js.map +0 -1
  182. package/dist/cli/commands/hydrogen/info.d.ts +0 -12
  183. package/dist/cli/commands/hydrogen/info.js +0 -28
  184. package/dist/cli/commands/hydrogen/info.js.map +0 -1
  185. package/dist/cli/commands/hydrogen/preview.d.ts +0 -13
  186. package/dist/cli/commands/hydrogen/preview.js +0 -46
  187. package/dist/cli/commands/hydrogen/preview.js.map +0 -1
  188. package/dist/cli/constants.d.ts +0 -15
  189. package/dist/cli/constants.js +0 -16
  190. package/dist/cli/constants.js.map +0 -1
  191. package/dist/cli/flags.d.ts +0 -4
  192. package/dist/cli/flags.js +0 -16
  193. package/dist/cli/flags.js.map +0 -1
  194. package/dist/cli/models/hydrogen.d.ts +0 -22
  195. package/dist/cli/models/hydrogen.js +0 -82
  196. package/dist/cli/models/hydrogen.js.map +0 -1
  197. package/dist/cli/prompts/git-init.d.ts +0 -1
  198. package/dist/cli/prompts/git-init.js +0 -16
  199. package/dist/cli/prompts/git-init.js.map +0 -1
  200. package/dist/cli/services/build/check-lockfile.d.ts +0 -3
  201. package/dist/cli/services/build/check-lockfile.js +0 -80
  202. package/dist/cli/services/build/check-lockfile.js.map +0 -1
  203. package/dist/cli/services/build.d.ts +0 -14
  204. package/dist/cli/services/build.js +0 -44
  205. package/dist/cli/services/build.js.map +0 -1
  206. package/dist/cli/services/deploy/config.d.ts +0 -4
  207. package/dist/cli/services/deploy/config.js +0 -49
  208. package/dist/cli/services/deploy/config.js.map +0 -1
  209. package/dist/cli/services/deploy/error.d.ts +0 -4
  210. package/dist/cli/services/deploy/error.js +0 -11
  211. package/dist/cli/services/deploy/error.js.map +0 -1
  212. package/dist/cli/services/deploy/graphql/create_deployment.d.ts +0 -10
  213. package/dist/cli/services/deploy/graphql/create_deployment.js +0 -15
  214. package/dist/cli/services/deploy/graphql/create_deployment.js.map +0 -1
  215. package/dist/cli/services/deploy/graphql/upload_deployment.d.ts +0 -1
  216. package/dist/cli/services/deploy/graphql/upload_deployment.js +0 -16
  217. package/dist/cli/services/deploy/graphql/upload_deployment.js.map +0 -1
  218. package/dist/cli/services/deploy/types.d.ts +0 -37
  219. package/dist/cli/services/deploy/types.js +0 -2
  220. package/dist/cli/services/deploy/types.js.map +0 -1
  221. package/dist/cli/services/deploy/upload.d.ts +0 -5
  222. package/dist/cli/services/deploy/upload.js +0 -81
  223. package/dist/cli/services/deploy/upload.js.map +0 -1
  224. package/dist/cli/services/deploy.d.ts +0 -2
  225. package/dist/cli/services/deploy.js +0 -103
  226. package/dist/cli/services/deploy.js.map +0 -1
  227. package/dist/cli/services/dev/check-version.d.ts +0 -1
  228. package/dist/cli/services/dev/check-version.js +0 -30
  229. package/dist/cli/services/dev/check-version.js.map +0 -1
  230. package/dist/cli/services/dev.d.ts +0 -10
  231. package/dist/cli/services/dev.js +0 -36
  232. package/dist/cli/services/dev.js.map +0 -1
  233. package/dist/cli/services/eslint.d.ts +0 -8
  234. package/dist/cli/services/eslint.js +0 -74
  235. package/dist/cli/services/eslint.js.map +0 -1
  236. package/dist/cli/services/info.d.ts +0 -7
  237. package/dist/cli/services/info.js +0 -131
  238. package/dist/cli/services/info.js.map +0 -1
  239. package/dist/cli/services/preview.d.ts +0 -12
  240. package/dist/cli/services/preview.js +0 -63
  241. package/dist/cli/services/preview.js.map +0 -1
  242. package/dist/cli/services/tailwind.d.ts +0 -9
  243. package/dist/cli/services/tailwind.js +0 -103
  244. package/dist/cli/services/tailwind.js.map +0 -1
  245. package/dist/cli/utilities/load-config.d.ts +0 -5
  246. package/dist/cli/utilities/load-config.js +0 -6
  247. package/dist/cli/utilities/load-config.js.map +0 -1
  248. package/dist/tsconfig.tsbuildinfo +0 -1
  249. package/oclif.manifest.json +0 -1
@@ -0,0 +1,320 @@
1
+ import {json, redirect, type ActionFunction} from '@shopify/remix-oxygen';
2
+ import {
3
+ Form,
4
+ useActionData,
5
+ useOutletContext,
6
+ useParams,
7
+ useTransition,
8
+ } from '@remix-run/react';
9
+ import {flattenConnection} from '@shopify/hydrogen-react';
10
+ import type {MailingAddressInput} from '@shopify/hydrogen-react/storefront-api-types';
11
+ import invariant from 'tiny-invariant';
12
+ import {Button, Text} from '~/components';
13
+ import {
14
+ createCustomerAddress,
15
+ deleteCustomerAddress,
16
+ updateCustomerAddress,
17
+ updateCustomerDefaultAddress,
18
+ } from '~/data';
19
+ import {getInputStyleClasses} from '~/lib/utils';
20
+ import type {AccountOutletContext} from '../edit';
21
+
22
+ interface ActionData {
23
+ formError?: string;
24
+ }
25
+
26
+ const badRequest = (data: ActionData) => json(data, {status: 400});
27
+
28
+ export const handle = {
29
+ renderInModal: true,
30
+ };
31
+
32
+ export const action: ActionFunction = async ({request, context, params}) => {
33
+ const formData = await request.formData();
34
+
35
+ const customerAccessToken = await context.session.get('customerAccessToken');
36
+ invariant(customerAccessToken, 'You must be logged in to edit your account.');
37
+
38
+ const addressId = formData.get('addressId');
39
+ invariant(typeof addressId === 'string', 'You must provide an address id.');
40
+
41
+ if (request.method === 'DELETE') {
42
+ try {
43
+ await deleteCustomerAddress(context, {
44
+ customerAccessToken,
45
+ addressId,
46
+ });
47
+
48
+ return redirect(params.lang ? `${params.lang}/account` : '/account');
49
+ } catch (error: any) {
50
+ return badRequest({formError: error.message});
51
+ }
52
+ }
53
+
54
+ const address: MailingAddressInput = {};
55
+
56
+ const keys: (keyof MailingAddressInput)[] = [
57
+ 'lastName',
58
+ 'firstName',
59
+ 'address1',
60
+ 'address2',
61
+ 'city',
62
+ 'province',
63
+ 'country',
64
+ 'zip',
65
+ 'phone',
66
+ 'company',
67
+ ];
68
+
69
+ for (const key of keys) {
70
+ const value = formData.get(key);
71
+ if (typeof value === 'string') {
72
+ address[key] = value;
73
+ }
74
+ }
75
+
76
+ const defaultAddress = formData.get('defaultAddress');
77
+
78
+ if (addressId === 'add') {
79
+ try {
80
+ const id = await createCustomerAddress(context, {
81
+ customerAccessToken,
82
+ address,
83
+ });
84
+
85
+ if (defaultAddress) {
86
+ await updateCustomerDefaultAddress(context, {
87
+ customerAccessToken,
88
+ addressId: id,
89
+ });
90
+ }
91
+
92
+ return redirect(params.lang ? `${params.lang}/account` : '/account');
93
+ } catch (error: any) {
94
+ return badRequest({formError: error.message});
95
+ }
96
+ } else {
97
+ try {
98
+ await updateCustomerAddress(context, {
99
+ customerAccessToken,
100
+ addressId: decodeURIComponent(addressId),
101
+ address,
102
+ });
103
+
104
+ if (defaultAddress) {
105
+ await updateCustomerDefaultAddress(context, {
106
+ customerAccessToken,
107
+ addressId: decodeURIComponent(addressId),
108
+ });
109
+ }
110
+
111
+ return redirect(params.lang ? `${params.lang}/account` : '/account');
112
+ } catch (error: any) {
113
+ return badRequest({formError: error.message});
114
+ }
115
+ }
116
+ };
117
+
118
+ export default function EditAddress() {
119
+ const {addressId} = useParams();
120
+ const isNewAddress = addressId === 'add';
121
+ const actionData = useActionData<ActionData>();
122
+ const transition = useTransition();
123
+ const {customer} = useOutletContext<AccountOutletContext>();
124
+ const addresses = flattenConnection(customer.addresses);
125
+ const defaultAddress = customer.defaultAddress;
126
+ /**
127
+ * When a refresh happens (or a user visits this link directly), the URL
128
+ * is actually stale because it contains a special token. This means the data
129
+ * loaded by the parent and passed to the outlet contains a newer, fresher token,
130
+ * and we don't find a match. We update the `find` logic to just perform a match
131
+ * on the first (permanent) part of the ID.
132
+ */
133
+ const normalizedAddress = decodeURIComponent(addressId ?? '').split('?')[0];
134
+ const address = addresses.find((address) =>
135
+ address.id!.startsWith(normalizedAddress),
136
+ );
137
+
138
+ return (
139
+ <>
140
+ <Text className="mt-4 mb-6" as="h3" size="lead">
141
+ {isNewAddress ? 'Add address' : 'Edit address'}
142
+ </Text>
143
+ <div className="max-w-lg">
144
+ <Form method="post">
145
+ <input
146
+ type="hidden"
147
+ name="addressId"
148
+ value={address?.id ?? addressId}
149
+ />
150
+ {actionData?.formError && (
151
+ <div className="flex items-center justify-center mb-6 bg-red-100 rounded">
152
+ <p className="m-4 text-sm text-red-900">{actionData.formError}</p>
153
+ </div>
154
+ )}
155
+ <div className="mt-3">
156
+ <input
157
+ className={getInputStyleClasses()}
158
+ id="firstName"
159
+ name="firstName"
160
+ required
161
+ type="text"
162
+ autoComplete="given-name"
163
+ placeholder="First name"
164
+ aria-label="First name"
165
+ defaultValue={address?.firstName ?? ''}
166
+ />
167
+ </div>
168
+ <div className="mt-3">
169
+ <input
170
+ className={getInputStyleClasses()}
171
+ id="lastName"
172
+ name="lastName"
173
+ required
174
+ type="text"
175
+ autoComplete="family-name"
176
+ placeholder="Last name"
177
+ aria-label="Last name"
178
+ defaultValue={address?.lastName ?? ''}
179
+ />
180
+ </div>
181
+ <div className="mt-3">
182
+ <input
183
+ className={getInputStyleClasses()}
184
+ id="company"
185
+ name="company"
186
+ type="text"
187
+ autoComplete="organization"
188
+ placeholder="Company"
189
+ aria-label="Company"
190
+ defaultValue={address?.company ?? ''}
191
+ />
192
+ </div>
193
+ <div className="mt-3">
194
+ <input
195
+ className={getInputStyleClasses()}
196
+ id="address1"
197
+ name="address1"
198
+ type="text"
199
+ autoComplete="address-line1"
200
+ placeholder="Address line 1*"
201
+ required
202
+ aria-label="Address line 1"
203
+ defaultValue={address?.address1 ?? ''}
204
+ />
205
+ </div>
206
+ <div className="mt-3">
207
+ <input
208
+ className={getInputStyleClasses()}
209
+ id="address2"
210
+ name="address2"
211
+ type="text"
212
+ autoComplete="address-line2"
213
+ placeholder="Address line 2"
214
+ aria-label="Address line 2"
215
+ defaultValue={address?.address2 ?? ''}
216
+ />
217
+ </div>
218
+ <div className="mt-3">
219
+ <input
220
+ className={getInputStyleClasses()}
221
+ id="city"
222
+ name="city"
223
+ type="text"
224
+ required
225
+ autoComplete="address-level2"
226
+ placeholder="City"
227
+ aria-label="City"
228
+ defaultValue={address?.city ?? ''}
229
+ />
230
+ </div>
231
+ <div className="mt-3">
232
+ <input
233
+ className={getInputStyleClasses()}
234
+ id="province"
235
+ name="province"
236
+ type="text"
237
+ autoComplete="address-level1"
238
+ placeholder="State / Province"
239
+ required
240
+ aria-label="State"
241
+ defaultValue={address?.province ?? ''}
242
+ />
243
+ </div>
244
+ <div className="mt-3">
245
+ <input
246
+ className={getInputStyleClasses()}
247
+ id="zip"
248
+ name="zip"
249
+ type="text"
250
+ autoComplete="postal-code"
251
+ placeholder="Zip / Postal Code"
252
+ required
253
+ aria-label="Zip"
254
+ defaultValue={address?.zip ?? ''}
255
+ />
256
+ </div>
257
+ <div className="mt-3">
258
+ <input
259
+ className={getInputStyleClasses()}
260
+ id="country"
261
+ name="country"
262
+ type="text"
263
+ autoComplete="country-name"
264
+ placeholder="Country"
265
+ required
266
+ aria-label="Country"
267
+ defaultValue={address?.country ?? ''}
268
+ />
269
+ </div>
270
+ <div className="mt-3">
271
+ <input
272
+ className={getInputStyleClasses()}
273
+ id="phone"
274
+ name="phone"
275
+ type="tel"
276
+ autoComplete="tel"
277
+ placeholder="Phone"
278
+ aria-label="Phone"
279
+ defaultValue={address?.phone ?? ''}
280
+ />
281
+ </div>
282
+ <div className="mt-4">
283
+ <input
284
+ type="checkbox"
285
+ name="defaultAddress"
286
+ id="defaultAddress"
287
+ defaultChecked={defaultAddress?.id === address?.id}
288
+ className="border-gray-500 rounded-sm cursor-pointer border-1"
289
+ />
290
+ <label
291
+ className="inline-block ml-2 text-sm cursor-pointer"
292
+ htmlFor="defaultAddress"
293
+ >
294
+ Set as default address
295
+ </label>
296
+ </div>
297
+ <div className="mt-8">
298
+ <Button
299
+ className="w-full rounded focus:shadow-outline"
300
+ type="submit"
301
+ variant="primary"
302
+ disabled={transition.state !== 'idle'}
303
+ >
304
+ {transition.state !== 'idle' ? 'Saving' : 'Save'}
305
+ </Button>
306
+ </div>
307
+ <div>
308
+ <Button
309
+ to=".."
310
+ className="w-full mt-2 rounded focus:shadow-outline"
311
+ variant="secondary"
312
+ >
313
+ Cancel
314
+ </Button>
315
+ </div>
316
+ </Form>
317
+ </div>
318
+ </>
319
+ );
320
+ }
@@ -0,0 +1,273 @@
1
+ import {json, redirect, type ActionFunction} from '@shopify/remix-oxygen';
2
+ import {
3
+ useActionData,
4
+ Form,
5
+ useOutletContext,
6
+ useTransition,
7
+ } from '@remix-run/react';
8
+ import type {
9
+ Customer,
10
+ CustomerUpdateInput,
11
+ } from '@shopify/hydrogen-react/storefront-api-types';
12
+ import clsx from 'clsx';
13
+ import invariant from 'tiny-invariant';
14
+ import {Button, Text} from '~/components';
15
+ import {getCustomer, updateCustomer} from '~/data';
16
+ import {getInputStyleClasses} from '~/lib/utils';
17
+
18
+ export interface AccountOutletContext {
19
+ customer: Customer;
20
+ }
21
+
22
+ export interface ActionData {
23
+ success?: boolean;
24
+ formError?: string;
25
+ fieldErrors?: {
26
+ firstName?: string;
27
+ lastName?: string;
28
+ email?: string;
29
+ phone?: string;
30
+ currentPassword?: string;
31
+ newPassword?: string;
32
+ newPassword2?: string;
33
+ };
34
+ }
35
+
36
+ const badRequest = (data: ActionData) => json(data, {status: 400});
37
+
38
+ const formDataHas = (formData: FormData, key: string) => {
39
+ if (!formData.has(key)) return false;
40
+
41
+ const value = formData.get(key);
42
+ return typeof value === 'string' && value.length > 0;
43
+ };
44
+
45
+ export const handle = {
46
+ renderInModal: true,
47
+ };
48
+
49
+ export const action: ActionFunction = async ({request, context, params}) => {
50
+ const formData = await request.formData();
51
+
52
+ const customerAccessToken = await context.session.get('customerAccessToken');
53
+
54
+ invariant(
55
+ customerAccessToken,
56
+ 'You must be logged in to update your account details.',
57
+ );
58
+
59
+ // Double-check current user is logged in.
60
+ // Will throw a logout redirect if not.
61
+ await getCustomer(context, {customerAccessToken, request});
62
+
63
+ if (
64
+ formDataHas(formData, 'newPassword') &&
65
+ !formDataHas(formData, 'currentPassword')
66
+ ) {
67
+ return badRequest({
68
+ fieldErrors: {
69
+ currentPassword:
70
+ 'Please enter your current password before entering a new password.',
71
+ },
72
+ });
73
+ }
74
+
75
+ if (
76
+ formData.has('newPassword') &&
77
+ formData.get('newPassword') !== formData.get('newPassword2')
78
+ ) {
79
+ return badRequest({
80
+ fieldErrors: {
81
+ newPassword2: 'New passwords must match.',
82
+ },
83
+ });
84
+ }
85
+
86
+ try {
87
+ const customer: CustomerUpdateInput = {};
88
+
89
+ formDataHas(formData, 'firstName') &&
90
+ (customer.firstName = formData.get('firstName') as string);
91
+ formDataHas(formData, 'lastName') &&
92
+ (customer.lastName = formData.get('lastName') as string);
93
+ formDataHas(formData, 'email') &&
94
+ (customer.email = formData.get('email') as string);
95
+ formDataHas(formData, 'phone') &&
96
+ (customer.phone = formData.get('phone') as string);
97
+ formDataHas(formData, 'newPassword') &&
98
+ (customer.password = formData.get('newPassword') as string);
99
+
100
+ await updateCustomer(context, {customerAccessToken, customer});
101
+
102
+ return redirect(params?.lang ? `${params.lang}/account` : '/account');
103
+ } catch (error: any) {
104
+ return badRequest({formError: error.message});
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Since this component is nested in `accounts/`, it is rendered in a modal via `<Outlet>` in `account.tsx`.
110
+ *
111
+ * This allows us to:
112
+ * - preserve URL state (`/accounts/edit` when the modal is open)
113
+ * - co-locate the edit action with the edit form (rather than grouped in account.tsx)
114
+ * - use the `useOutletContext` hook to access the customer data from the parent route (no additional data loading)
115
+ * - return a simple `redirect()` from this action to close the modal :mindblown: (no useState/useEffect)
116
+ * - use the presence of outlet data (in `account.tsx`) to open/close the modal (no useState)
117
+ */
118
+ export default function AccountDetailsEdit() {
119
+ const actionData = useActionData<ActionData>();
120
+ const {customer} = useOutletContext<AccountOutletContext>();
121
+ const transition = useTransition();
122
+
123
+ return (
124
+ <>
125
+ <Text className="mt-4 mb-6" as="h3" size="lead">
126
+ Update your profile
127
+ </Text>
128
+ <Form method="post">
129
+ {actionData?.formError && (
130
+ <div className="flex items-center justify-center mb-6 bg-red-100 rounded">
131
+ <p className="m-4 text-sm text-red-900">{actionData.formError}</p>
132
+ </div>
133
+ )}
134
+ <div className="mt-3">
135
+ <input
136
+ className={getInputStyleClasses()}
137
+ id="firstName"
138
+ name="firstName"
139
+ type="text"
140
+ autoComplete="given-name"
141
+ placeholder="First name"
142
+ aria-label="First name"
143
+ defaultValue={customer.firstName ?? ''}
144
+ />
145
+ </div>
146
+ <div className="mt-3">
147
+ <input
148
+ className={getInputStyleClasses()}
149
+ id="lastName"
150
+ name="lastName"
151
+ type="text"
152
+ autoComplete="family-name"
153
+ placeholder="Last name"
154
+ aria-label="Last name"
155
+ defaultValue={customer.lastName ?? ''}
156
+ />
157
+ </div>
158
+ <div className="mt-3">
159
+ <input
160
+ className={getInputStyleClasses()}
161
+ id="phone"
162
+ name="phone"
163
+ type="tel"
164
+ autoComplete="tel"
165
+ placeholder="Mobile"
166
+ aria-label="Mobile"
167
+ defaultValue={customer.phone ?? ''}
168
+ />
169
+ </div>
170
+ <div className="mt-3">
171
+ <input
172
+ className={getInputStyleClasses(actionData?.fieldErrors?.email)}
173
+ id="email"
174
+ name="email"
175
+ type="email"
176
+ autoComplete="email"
177
+ required
178
+ placeholder="Email address"
179
+ aria-label="Email address"
180
+ defaultValue={customer.email ?? ''}
181
+ />
182
+ {actionData?.fieldErrors?.email && (
183
+ <p className="text-red-500 text-xs">
184
+ {actionData.fieldErrors.email} &nbsp;
185
+ </p>
186
+ )}
187
+ </div>
188
+ <Text className="mb-6 mt-6" as="h3" size="lead">
189
+ Change your password
190
+ </Text>
191
+ <Password
192
+ name="currentPassword"
193
+ label="Current password"
194
+ passwordError={actionData?.fieldErrors?.currentPassword}
195
+ />
196
+ {actionData?.fieldErrors?.currentPassword && (
197
+ <Text size="fine" className="mt-1 text-red-500">
198
+ {actionData.fieldErrors.currentPassword} &nbsp;
199
+ </Text>
200
+ )}
201
+ <Password
202
+ name="newPassword"
203
+ label="New password"
204
+ passwordError={actionData?.fieldErrors?.newPassword}
205
+ />
206
+ <Password
207
+ name="newPassword2"
208
+ label="Re-enter new password"
209
+ passwordError={actionData?.fieldErrors?.newPassword2}
210
+ />
211
+ <Text
212
+ size="fine"
213
+ color="subtle"
214
+ className={clsx(
215
+ 'mt-1',
216
+ actionData?.fieldErrors?.newPassword && 'text-red-500',
217
+ )}
218
+ >
219
+ Passwords must be at least 8 characters.
220
+ </Text>
221
+ {actionData?.fieldErrors?.newPassword2 ? <br /> : null}
222
+ {actionData?.fieldErrors?.newPassword2 && (
223
+ <Text size="fine" className="mt-1 text-red-500">
224
+ {actionData.fieldErrors.newPassword2} &nbsp;
225
+ </Text>
226
+ )}
227
+ <div className="mt-6">
228
+ <Button
229
+ className="text-sm mb-2"
230
+ variant="primary"
231
+ width="full"
232
+ type="submit"
233
+ disabled={transition.state !== 'idle'}
234
+ >
235
+ {transition.state !== 'idle' ? 'Saving' : 'Save'}
236
+ </Button>
237
+ </div>
238
+ <div className="mb-4">
239
+ <Button to=".." className="text-sm" variant="secondary" width="full">
240
+ Cancel
241
+ </Button>
242
+ </div>
243
+ </Form>
244
+ </>
245
+ );
246
+ }
247
+
248
+ function Password({
249
+ name,
250
+ passwordError,
251
+ label,
252
+ }: {
253
+ name: string;
254
+ passwordError?: string;
255
+ label: string;
256
+ }) {
257
+ return (
258
+ <div className="mt-3">
259
+ <input
260
+ className={getInputStyleClasses(passwordError)}
261
+ id={name}
262
+ name={name}
263
+ type="password"
264
+ autoComplete={
265
+ name === 'currentPassword' ? 'current-password' : undefined
266
+ }
267
+ placeholder={label}
268
+ aria-label={label}
269
+ minLength={8}
270
+ />
271
+ </div>
272
+ );
273
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ redirect,
3
+ type ActionFunction,
4
+ type AppLoadContext,
5
+ type LoaderArgs,
6
+ } from '@shopify/remix-oxygen';
7
+ import {getLocaleFromRequest} from '~/lib/utils';
8
+
9
+ export async function logout(request: Request, context: AppLoadContext) {
10
+ const {session} = context;
11
+ session.unset('customerAccessToken');
12
+
13
+ const {pathPrefix} = getLocaleFromRequest(request);
14
+
15
+ return redirect(`${pathPrefix}/account/login`, {
16
+ headers: {
17
+ 'Set-Cookie': await session.commit(),
18
+ },
19
+ });
20
+ }
21
+
22
+ export async function loader({request}: LoaderArgs) {
23
+ const {pathPrefix} = getLocaleFromRequest(request);
24
+ return redirect(pathPrefix);
25
+ }
26
+
27
+ export const action: ActionFunction = async ({request, context}) => {
28
+ return logout(request, context);
29
+ };