@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,289 @@
1
+ import type {CustomerFragment} from 'storefrontapi.generated';
2
+ import type {CustomerUpdateInput} from '@shopify/hydrogen/storefront-api-types';
3
+ import type {ActionArgs, LoaderArgs} from '@shopify/remix-oxygen';
4
+ import {json, redirect, type V2_MetaFunction} from '@shopify/remix-oxygen';
5
+ import {
6
+ Form,
7
+ useActionData,
8
+ useNavigation,
9
+ useOutletContext,
10
+ } from '@remix-run/react';
11
+
12
+ export type ActionResponse = {
13
+ error: string | null;
14
+ customer: CustomerFragment | null;
15
+ };
16
+
17
+ export const meta: V2_MetaFunction = () => {
18
+ return [{title: 'Profile'}];
19
+ };
20
+
21
+ export async function loader({context}: LoaderArgs) {
22
+ const customerAccessToken = await context.session.get('customerAccessToken');
23
+ if (!customerAccessToken) {
24
+ return redirect('/account/login');
25
+ }
26
+ return json({});
27
+ }
28
+
29
+ export async function action({request, context}: ActionArgs) {
30
+ const {session, storefront} = context;
31
+
32
+ if (request.method !== 'PUT') {
33
+ return json({error: 'Method not allowed'}, {status: 405});
34
+ }
35
+
36
+ const form = await request.formData();
37
+ const customerAccessToken = await session.get('customerAccessToken');
38
+ if (!customerAccessToken) {
39
+ return json({error: 'Unauthorized'}, {status: 401});
40
+ }
41
+
42
+ try {
43
+ const password = getPassword(form);
44
+ const customer: CustomerUpdateInput = {};
45
+ const validInputKeys = [
46
+ 'firstName',
47
+ 'lastName',
48
+ 'email',
49
+ 'password',
50
+ 'phone',
51
+ ] as const;
52
+ for (const [key, value] of form.entries()) {
53
+ if (!validInputKeys.includes(key as any)) {
54
+ continue;
55
+ }
56
+ if (key === 'acceptsMarketing') {
57
+ customer.acceptsMarketing = value === 'on';
58
+ }
59
+ if (typeof value === 'string' && value.length) {
60
+ customer[key as (typeof validInputKeys)[number]] = value;
61
+ }
62
+ }
63
+
64
+ if (password) {
65
+ customer.password = password;
66
+ }
67
+
68
+ // update customer and possibly password
69
+ const updated = await storefront.mutate(CUSTOMER_UPDATE_MUTATION, {
70
+ variables: {
71
+ customerAccessToken: customerAccessToken.accessToken,
72
+ customer,
73
+ },
74
+ });
75
+
76
+ // check for mutation errors
77
+ if (updated.customerUpdate?.customerUserErrors?.length) {
78
+ return json(
79
+ {error: updated.customerUpdate?.customerUserErrors[0]},
80
+ {status: 400},
81
+ );
82
+ }
83
+
84
+ // update session with the updated access token
85
+ if (updated.customerUpdate?.customerAccessToken?.accessToken) {
86
+ session.set(
87
+ 'customerAccessToken',
88
+ updated.customerUpdate?.customerAccessToken,
89
+ );
90
+ }
91
+
92
+ return json(
93
+ {error: null, customer: updated.customerUpdate?.customer},
94
+ {
95
+ headers: {
96
+ 'Set-Cookie': await session.commit(),
97
+ },
98
+ },
99
+ );
100
+ } catch (error: any) {
101
+ return json({error: error.message, customer: null}, {status: 400});
102
+ }
103
+ }
104
+
105
+ export default function AccountProfile() {
106
+ const account = useOutletContext<{customer: CustomerFragment}>();
107
+ const {state} = useNavigation();
108
+ const action = useActionData<ActionResponse>();
109
+ const customer = action?.customer ?? account?.customer;
110
+
111
+ return (
112
+ <div className="account-profile">
113
+ <h2>My profile</h2>
114
+ <br />
115
+ <Form method="PUT">
116
+ <legend>Personal information</legend>
117
+ <fieldset>
118
+ <label htmlFor="firstName">First name</label>
119
+ <input
120
+ id="firstName"
121
+ name="firstName"
122
+ type="text"
123
+ autoComplete="given-name"
124
+ placeholder="First name"
125
+ aria-label="First name"
126
+ defaultValue={customer.firstName ?? ''}
127
+ minLength={2}
128
+ />
129
+ <label htmlFor="lastName">Last name</label>
130
+ <input
131
+ id="lastName"
132
+ name="lastName"
133
+ type="text"
134
+ autoComplete="family-name"
135
+ placeholder="Last name"
136
+ aria-label="Last name"
137
+ defaultValue={customer.lastName ?? ''}
138
+ minLength={2}
139
+ />
140
+ <label htmlFor="phone">Mobile</label>
141
+ <input
142
+ id="phone"
143
+ name="phone"
144
+ type="tel"
145
+ autoComplete="tel"
146
+ placeholder="Mobile"
147
+ aria-label="Mobile"
148
+ defaultValue={customer.phone ?? ''}
149
+ />
150
+ <label htmlFor="email">Email address</label>
151
+ <input
152
+ id="email"
153
+ name="email"
154
+ type="email"
155
+ autoComplete="email"
156
+ required
157
+ placeholder="Email address"
158
+ aria-label="Email address"
159
+ defaultValue={customer.email ?? ''}
160
+ />
161
+ <div className="account-profile-marketing">
162
+ <input
163
+ id="acceptsMarketing"
164
+ name="acceptsMarketing"
165
+ type="checkbox"
166
+ placeholder="Accept marketing"
167
+ aria-label="Accept marketing"
168
+ defaultChecked={customer.acceptsMarketing}
169
+ />
170
+ <label htmlFor="acceptsMarketing">
171
+ &nbsp; Subscribed to marketing communications
172
+ </label>
173
+ </div>
174
+ </fieldset>
175
+ <br />
176
+ <legend>Change password (optional)</legend>
177
+ <fieldset>
178
+ <label htmlFor="currentPassword">Current password</label>
179
+ <input
180
+ id="currentPassword"
181
+ name="currentPassword"
182
+ type="password"
183
+ autoComplete="current-password"
184
+ placeholder="Current password"
185
+ aria-label="Current password"
186
+ minLength={8}
187
+ />
188
+
189
+ <label htmlFor="newPassword">New password</label>
190
+ <input
191
+ id="newPassword"
192
+ name="newPassword"
193
+ type="password"
194
+ placeholder="New password"
195
+ aria-label="New password"
196
+ minLength={8}
197
+ />
198
+
199
+ <label htmlFor="newPasswordConfirm">New password (confirm)</label>
200
+ <input
201
+ id="newPasswordConfirm"
202
+ name="newPasswordConfirm"
203
+ type="password"
204
+ placeholder="New password (confirm)"
205
+ aria-label="New password confirm"
206
+ minLength={8}
207
+ />
208
+ <small>Passwords must be at least 8 characters.</small>
209
+ </fieldset>
210
+ {action?.error ? (
211
+ <p>
212
+ <mark>
213
+ <small>{action.error}</small>
214
+ </mark>
215
+ </p>
216
+ ) : (
217
+ <br />
218
+ )}
219
+ <button type="submit" disabled={state !== 'idle'}>
220
+ {state !== 'idle' ? 'Updating' : 'Update'}
221
+ </button>
222
+ </Form>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ function getPassword(form: FormData): string | undefined {
228
+ let password;
229
+ const currentPassword = form.get('currentPassword');
230
+ const newPassword = form.get('newPassword');
231
+ const newPasswordConfirm = form.get('newPasswordConfirm');
232
+
233
+ let passwordError;
234
+ if (newPassword && !currentPassword) {
235
+ passwordError = new Error('Current password is required.');
236
+ }
237
+
238
+ if (newPassword && newPassword !== newPasswordConfirm) {
239
+ passwordError = new Error('New passwords must match.');
240
+ }
241
+
242
+ if (newPassword && currentPassword && newPassword === currentPassword) {
243
+ passwordError = new Error(
244
+ 'New password must be different than current password.',
245
+ );
246
+ }
247
+
248
+ if (passwordError) {
249
+ throw passwordError;
250
+ }
251
+
252
+ if (currentPassword && newPassword) {
253
+ password = newPassword;
254
+ } else {
255
+ password = currentPassword;
256
+ }
257
+
258
+ return String(password);
259
+ }
260
+
261
+ const CUSTOMER_UPDATE_MUTATION = `#graphql
262
+ # https://shopify.dev/docs/api/storefront/latest/mutations/customerUpdate
263
+ mutation customerUpdate(
264
+ $customerAccessToken: String!,
265
+ $customer: CustomerUpdateInput!
266
+ $country: CountryCode
267
+ $language: LanguageCode
268
+ ) @inContext(language: $language, country: $country) {
269
+ customerUpdate(customerAccessToken: $customerAccessToken, customer: $customer) {
270
+ customer {
271
+ acceptsMarketing
272
+ email
273
+ firstName
274
+ id
275
+ lastName
276
+ phone
277
+ }
278
+ customerAccessToken {
279
+ accessToken
280
+ expiresAt
281
+ }
282
+ customerUserErrors {
283
+ code
284
+ field
285
+ message
286
+ }
287
+ }
288
+ }
289
+ ` as const;
@@ -0,0 +1,203 @@
1
+ import {Form, NavLink, Outlet, useLoaderData} from '@remix-run/react';
2
+ import {json, redirect, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import type {CustomerFragment} from 'storefrontapi.generated';
4
+
5
+ export function shouldRevalidate() {
6
+ return true;
7
+ }
8
+
9
+ export async function loader({request, context}: LoaderArgs) {
10
+ const {session, storefront} = context;
11
+ const {pathname} = new URL(request.url);
12
+ const customerAccessToken = await session.get('customerAccessToken');
13
+ const isLoggedIn = Boolean(customerAccessToken?.accessToken);
14
+ const isAccountHome = pathname === '/account' || pathname === '/account/';
15
+ const isPrivateRoute =
16
+ /^\/account\/(orders|orders\/.*|profile|addresses|addresses\/.*)$/.test(
17
+ pathname,
18
+ );
19
+
20
+ if (!isLoggedIn) {
21
+ if (isPrivateRoute || isAccountHome) {
22
+ session.unset('customerAccessToken');
23
+ return redirect('/account/login', {
24
+ headers: {
25
+ 'Set-Cookie': await session.commit(),
26
+ },
27
+ });
28
+ } else {
29
+ // public subroute such as /account/login...
30
+ return json({
31
+ isLoggedIn: false,
32
+ isAccountHome,
33
+ isPrivateRoute,
34
+ customer: null,
35
+ });
36
+ }
37
+ } else {
38
+ // loggedIn, default redirect to the orders page
39
+ if (isAccountHome) {
40
+ return redirect('/account/orders');
41
+ }
42
+ }
43
+
44
+ try {
45
+ const {customer} = await storefront.query(CUSTOMER_QUERY, {
46
+ variables: {
47
+ customerAccessToken: customerAccessToken.accessToken,
48
+ country: storefront.i18n.country,
49
+ language: storefront.i18n.language,
50
+ },
51
+ cache: storefront.CacheNone(),
52
+ });
53
+
54
+ if (!customer) {
55
+ throw new Error('Customer not found');
56
+ }
57
+
58
+ return json(
59
+ {isLoggedIn, isPrivateRoute, isAccountHome, customer},
60
+ {
61
+ headers: {
62
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
63
+ },
64
+ },
65
+ );
66
+ } catch (error) {
67
+ // eslint-disable-next-line no-console
68
+ console.error('There was a problem loading account', error);
69
+ session.unset('customerAccessToken');
70
+ return redirect('/account/login', {
71
+ headers: {
72
+ 'Set-Cookie': await session.commit(),
73
+ },
74
+ });
75
+ }
76
+ }
77
+
78
+ export default function Acccount() {
79
+ const {customer, isPrivateRoute, isAccountHome} =
80
+ useLoaderData<typeof loader>();
81
+
82
+ if (!isPrivateRoute && !isAccountHome) {
83
+ return <Outlet context={{customer}} />;
84
+ }
85
+
86
+ return (
87
+ <AccountLayout customer={customer as CustomerFragment}>
88
+ <br />
89
+ <br />
90
+ <Outlet context={{customer}} />
91
+ </AccountLayout>
92
+ );
93
+ }
94
+
95
+ function AccountLayout({
96
+ customer,
97
+ children,
98
+ }: {
99
+ customer: CustomerFragment;
100
+ children: React.ReactNode;
101
+ }) {
102
+ const heading = customer
103
+ ? customer.firstName
104
+ ? `Welcome, ${customer.firstName}`
105
+ : `Welcome to your account.`
106
+ : 'Account Details';
107
+
108
+ return (
109
+ <div className="account">
110
+ <h1>{heading}</h1>
111
+ <br />
112
+ <AcccountMenu />
113
+ {children}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ function AcccountMenu() {
119
+ function isActiveStyle({
120
+ isActive,
121
+ isPending,
122
+ }: {
123
+ isActive: boolean;
124
+ isPending: boolean;
125
+ }) {
126
+ return {
127
+ fontWeight: isActive ? 'bold' : '',
128
+ color: isPending ? 'grey' : 'black',
129
+ };
130
+ }
131
+ return (
132
+ <nav role="navigation">
133
+ <NavLink to="/account/orders" style={isActiveStyle}>
134
+ Orders &nbsp;
135
+ </NavLink>
136
+ &nbsp;|&nbsp;
137
+ <NavLink to="/account/profile" style={isActiveStyle}>
138
+ &nbsp; Profile &nbsp;
139
+ </NavLink>
140
+ &nbsp;|&nbsp;
141
+ <NavLink to="/account/addresses" style={isActiveStyle}>
142
+ &nbsp; Addresses &nbsp;
143
+ </NavLink>
144
+ &nbsp;|&nbsp;
145
+ <Logout />
146
+ </nav>
147
+ );
148
+ }
149
+
150
+ function Logout() {
151
+ return (
152
+ <Form className="account-logout" method="POST" action="/account/logout">
153
+ &nbsp;<button type="submit">Sign out</button>
154
+ </Form>
155
+ );
156
+ }
157
+
158
+ export const CUSTOMER_FRAGMENT = `#graphql
159
+ fragment Customer on Customer {
160
+ acceptsMarketing
161
+ addresses(first: 6) {
162
+ nodes {
163
+ ...Address
164
+ }
165
+ }
166
+ defaultAddress {
167
+ ...Address
168
+ }
169
+ email
170
+ firstName
171
+ lastName
172
+ numberOfOrders
173
+ phone
174
+ }
175
+ fragment Address on MailingAddress {
176
+ id
177
+ formatted
178
+ firstName
179
+ lastName
180
+ company
181
+ address1
182
+ address2
183
+ country
184
+ province
185
+ city
186
+ zip
187
+ phone
188
+ }
189
+ ` as const;
190
+
191
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/queries/customer
192
+ const CUSTOMER_QUERY = `#graphql
193
+ query Customer(
194
+ $customerAccessToken: String!
195
+ $country: CountryCode
196
+ $language: LanguageCode
197
+ ) @inContext(country: $country, language: $language) {
198
+ customer(customerAccessToken: $customerAccessToken) {
199
+ ...Customer
200
+ }
201
+ }
202
+ ${CUSTOMER_FRAGMENT}
203
+ ` as const;
@@ -0,0 +1,157 @@
1
+ import type {ActionArgs, LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {json, redirect} from '@shopify/remix-oxygen';
3
+ import {Form, useActionData, type V2_MetaFunction} from '@remix-run/react';
4
+
5
+ type ActionResponse = {
6
+ error: string | null;
7
+ };
8
+
9
+ export const meta: V2_MetaFunction = () => {
10
+ return [{title: 'Activate Account'}];
11
+ };
12
+
13
+ export async function loader({context}: LoaderArgs) {
14
+ if (await context.session.get('customerAccessToken')) {
15
+ return redirect('/account');
16
+ }
17
+ return json({});
18
+ }
19
+
20
+ export async function action({request, context, params}: ActionArgs) {
21
+ const {session, storefront} = context;
22
+ const {id, activationToken} = params;
23
+
24
+ if (request.method !== 'POST') {
25
+ return json({error: 'Method not allowed'}, {status: 405});
26
+ }
27
+
28
+ try {
29
+ if (!id || !activationToken) {
30
+ throw new Error('Missing token. The link you followed might be wrong.');
31
+ }
32
+
33
+ const form = await request.formData();
34
+ const password = form.has('password') ? String(form.get('password')) : null;
35
+ const passwordConfirm = form.has('passwordConfirm')
36
+ ? String(form.get('passwordConfirm'))
37
+ : null;
38
+
39
+ const validPasswords =
40
+ password && passwordConfirm && password === passwordConfirm;
41
+
42
+ if (!validPasswords) {
43
+ throw new Error('Passwords do not match');
44
+ }
45
+
46
+ const {customerActivate} = await storefront.mutate(
47
+ CUSTOMER_ACTIVATE_MUTATION,
48
+ {
49
+ variables: {
50
+ id: `gid://shopify/Customer/${id}`,
51
+ input: {
52
+ password,
53
+ activationToken,
54
+ },
55
+ },
56
+ },
57
+ );
58
+
59
+ if (customerActivate?.customerUserErrors?.length) {
60
+ throw new Error(customerActivate.customerUserErrors[0].message);
61
+ }
62
+
63
+ const {customerAccessToken} = customerActivate ?? {};
64
+ if (!customerAccessToken) {
65
+ throw new Error('Could not activate account.');
66
+ }
67
+ session.set('customerAccessToken', customerAccessToken);
68
+
69
+ return redirect('/account', {
70
+ headers: {
71
+ 'Set-Cookie': await session.commit(),
72
+ },
73
+ });
74
+ } catch (error: unknown) {
75
+ if (error instanceof Error) {
76
+ return json({error: error.message}, {status: 400});
77
+ }
78
+ return json({error}, {status: 400});
79
+ }
80
+ }
81
+
82
+ export default function Activate() {
83
+ const action = useActionData<ActionResponse>();
84
+ const error = action?.error ?? null;
85
+
86
+ return (
87
+ <div className="account-activate">
88
+ <h1>Activate Account.</h1>
89
+ <p>Create your password to activate your account.</p>
90
+ <Form method="POST">
91
+ <fieldset>
92
+ <label htmlFor="password">Password</label>
93
+ <input
94
+ id="password"
95
+ name="password"
96
+ type="password"
97
+ autoComplete="current-password"
98
+ placeholder="Password"
99
+ aria-label="Password"
100
+ minLength={8}
101
+ required
102
+ // eslint-disable-next-line jsx-a11y/no-autofocus
103
+ autoFocus
104
+ />
105
+ <label htmlFor="passwordConfirm">Re-enter password</label>
106
+ <input
107
+ id="passwordConfirm"
108
+ name="passwordConfirm"
109
+ type="password"
110
+ autoComplete="current-password"
111
+ placeholder="Re-enter password"
112
+ aria-label="Re-enter password"
113
+ minLength={8}
114
+ required
115
+ />
116
+ </fieldset>
117
+ {error ? (
118
+ <p>
119
+ <mark>
120
+ <small>{error}</small>
121
+ </mark>
122
+ </p>
123
+ ) : (
124
+ <br />
125
+ )}
126
+ <button
127
+ className="bg-primary text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
128
+ type="submit"
129
+ >
130
+ Save
131
+ </button>
132
+ </Form>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customeractivate
138
+ const CUSTOMER_ACTIVATE_MUTATION = `#graphql
139
+ mutation customerActivate(
140
+ $id: ID!,
141
+ $input: CustomerActivateInput!,
142
+ $country: CountryCode,
143
+ $language: LanguageCode
144
+ ) @inContext(country: $country, language: $language) {
145
+ customerActivate(id: $id, input: $input) {
146
+ customerAccessToken {
147
+ accessToken
148
+ expiresAt
149
+ }
150
+ customerUserErrors {
151
+ code
152
+ field
153
+ message
154
+ }
155
+ }
156
+ }
157
+ ` as const;