@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,143 @@
1
+ import {
2
+ json,
3
+ redirect,
4
+ type ActionArgs,
5
+ type LoaderArgs,
6
+ type V2_MetaFunction,
7
+ } from '@shopify/remix-oxygen';
8
+ import {Form, Link, useActionData} from '@remix-run/react';
9
+
10
+ type ActionResponse = {
11
+ error: string | null;
12
+ };
13
+
14
+ export const meta: V2_MetaFunction = () => {
15
+ return [{title: 'Login'}];
16
+ };
17
+
18
+ export async function loader({context}: LoaderArgs) {
19
+ if (await context.session.get('customerAccessToken')) {
20
+ return redirect('/account');
21
+ }
22
+ return json({});
23
+ }
24
+
25
+ export async function action({request, context}: ActionArgs) {
26
+ const {session, storefront} = context;
27
+
28
+ if (request.method !== 'POST') {
29
+ return json({error: 'Method not allowed'}, {status: 405});
30
+ }
31
+
32
+ try {
33
+ const form = await request.formData();
34
+ const email = String(form.has('email') ? form.get('email') : '');
35
+ const password = String(form.has('password') ? form.get('password') : '');
36
+ const validInputs = Boolean(email && password);
37
+
38
+ if (!validInputs) {
39
+ throw new Error('Please provide both an email and a password.');
40
+ }
41
+
42
+ const {customerAccessTokenCreate} = await storefront.mutate(
43
+ LOGIN_MUTATION,
44
+ {
45
+ variables: {
46
+ input: {email, password},
47
+ },
48
+ },
49
+ );
50
+
51
+ if (!customerAccessTokenCreate?.customerAccessToken?.accessToken) {
52
+ throw new Error(customerAccessTokenCreate?.customerUserErrors[0].message);
53
+ }
54
+
55
+ const {customerAccessToken} = customerAccessTokenCreate;
56
+ session.set('customerAccessToken', customerAccessToken);
57
+
58
+ return redirect('/account', {
59
+ headers: {
60
+ 'Set-Cookie': await session.commit(),
61
+ },
62
+ });
63
+ } catch (error: unknown) {
64
+ if (error instanceof Error) {
65
+ return json({error: error.message}, {status: 400});
66
+ }
67
+ return json({error}, {status: 400});
68
+ }
69
+ }
70
+
71
+ export default function Login() {
72
+ const data = useActionData<ActionResponse>();
73
+ const error = data?.error || null;
74
+
75
+ return (
76
+ <div className="login">
77
+ <h1>Sign in.</h1>
78
+ <Form method="POST">
79
+ <fieldset>
80
+ <label htmlFor="email">Email address</label>
81
+ <input
82
+ id="email"
83
+ name="email"
84
+ type="email"
85
+ autoComplete="email"
86
+ required
87
+ placeholder="Email address"
88
+ aria-label="Email address"
89
+ // eslint-disable-next-line jsx-a11y/no-autofocus
90
+ autoFocus
91
+ />
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
+ />
103
+ </fieldset>
104
+ {error ? (
105
+ <p>
106
+ <mark>
107
+ <small>{error}</small>
108
+ </mark>
109
+ </p>
110
+ ) : (
111
+ <br />
112
+ )}
113
+ <button type="submit">Sign in</button>
114
+ </Form>
115
+ <br />
116
+ <div>
117
+ <p>
118
+ <Link to="/account/recover">Forgot password →</Link>
119
+ </p>
120
+ <p>
121
+ <Link to="/account/register">Register →</Link>
122
+ </p>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customeraccesstokencreate
129
+ const LOGIN_MUTATION = `#graphql
130
+ mutation login($input: CustomerAccessTokenCreateInput!) {
131
+ customerAccessTokenCreate(input: $input) {
132
+ customerUserErrors {
133
+ code
134
+ field
135
+ message
136
+ }
137
+ customerAccessToken {
138
+ accessToken
139
+ expiresAt
140
+ }
141
+ }
142
+ }
143
+ ` as const;
@@ -0,0 +1,33 @@
1
+ import {
2
+ json,
3
+ redirect,
4
+ type ActionArgs,
5
+ type V2_MetaFunction,
6
+ } from '@shopify/remix-oxygen';
7
+
8
+ export const meta: V2_MetaFunction = () => {
9
+ return [{title: 'Logout'}];
10
+ };
11
+
12
+ export async function loader() {
13
+ return redirect('/account/login');
14
+ }
15
+
16
+ export async function action({request, context}: ActionArgs) {
17
+ const {session} = context;
18
+ session.unset('customerAccessToken');
19
+
20
+ if (request.method !== 'POST') {
21
+ return json({error: 'Method not allowed'}, {status: 405});
22
+ }
23
+
24
+ return redirect('/', {
25
+ headers: {
26
+ 'Set-Cookie': await session.commit(),
27
+ },
28
+ });
29
+ }
30
+
31
+ export default function Logout() {
32
+ return null;
33
+ }
@@ -0,0 +1,124 @@
1
+ import {json, redirect, type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {Form, Link, useActionData} from '@remix-run/react';
3
+
4
+ type ActionResponse = {
5
+ error?: string;
6
+ resetRequested?: boolean;
7
+ };
8
+
9
+ export async function loader({context}: LoaderArgs) {
10
+ const customerAccessToken = await context.session.get('customerAccessToken');
11
+ if (customerAccessToken) {
12
+ return redirect('/account');
13
+ }
14
+
15
+ return json({});
16
+ }
17
+
18
+ export async function action({request, context}: LoaderArgs) {
19
+ const {storefront} = context;
20
+ const form = await request.formData();
21
+ const email = form.has('email') ? String(form.get('email')) : null;
22
+
23
+ if (request.method !== 'POST') {
24
+ return json({error: 'Method not allowed'}, {status: 405});
25
+ }
26
+
27
+ try {
28
+ if (!email) {
29
+ throw new Error('Please provide an email.');
30
+ }
31
+ await storefront.mutate(CUSTOMER_RECOVER_MUTATION, {
32
+ variables: {email},
33
+ });
34
+
35
+ return json({resetRequested: true});
36
+ } catch (error: unknown) {
37
+ const resetRequested = false;
38
+ if (error instanceof Error) {
39
+ return json({error: error.message, resetRequested}, {status: 400});
40
+ }
41
+ return json({error, resetRequested}, {status: 400});
42
+ }
43
+ }
44
+
45
+ export default function Recover() {
46
+ const action = useActionData<ActionResponse>();
47
+
48
+ return (
49
+ <div className="account-recover">
50
+ <div>
51
+ {action?.resetRequested ? (
52
+ <>
53
+ <h1>Request Sent.</h1>
54
+ <p>
55
+ If that email address is in our system, you will receive an email
56
+ with instructions about how to reset your password in a few
57
+ minutes.
58
+ </p>
59
+ <br />
60
+ <Link to="/account/login">Return to Login</Link>
61
+ </>
62
+ ) : (
63
+ <>
64
+ <h1>Forgot Password.</h1>
65
+ <p>
66
+ Enter the email address associated with your account to receive a
67
+ link to reset your password.
68
+ </p>
69
+ <br />
70
+ <Form method="POST">
71
+ <fieldset>
72
+ <label htmlFor="email">Email</label>
73
+ <input
74
+ aria-label="Email address"
75
+ autoComplete="email"
76
+ // eslint-disable-next-line jsx-a11y/no-autofocus
77
+ autoFocus
78
+ id="email"
79
+ name="email"
80
+ placeholder="Email address"
81
+ required
82
+ type="email"
83
+ />
84
+ </fieldset>
85
+ {action?.error ? (
86
+ <p>
87
+ <mark>
88
+ <small>{action.error}</small>
89
+ </mark>
90
+ </p>
91
+ ) : (
92
+ <br />
93
+ )}
94
+ <button type="submit">Request Reset Link</button>
95
+ </Form>
96
+ <div>
97
+ <br />
98
+ <p>
99
+ <Link to="/account/login">Login →</Link>
100
+ </p>
101
+ </div>
102
+ </>
103
+ )}
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerrecover
110
+ const CUSTOMER_RECOVER_MUTATION = `#graphql
111
+ mutation customerRecover(
112
+ $email: String!,
113
+ $country: CountryCode,
114
+ $language: LanguageCode
115
+ ) @inContext(country: $country, language: $language) {
116
+ customerRecover(email: $email) {
117
+ customerUserErrors {
118
+ code
119
+ field
120
+ message
121
+ }
122
+ }
123
+ }
124
+ ` as const;
@@ -0,0 +1,207 @@
1
+ import {
2
+ json,
3
+ redirect,
4
+ type ActionFunction,
5
+ type LoaderArgs,
6
+ } from '@shopify/remix-oxygen';
7
+ import {Form, Link, useActionData} from '@remix-run/react';
8
+ import type {CustomerCreateMutation} from 'storefrontapi.generated';
9
+
10
+ type ActionResponse = {
11
+ error: string | null;
12
+ newCustomer:
13
+ | NonNullable<CustomerCreateMutation['customerCreate']>['customer']
14
+ | null;
15
+ };
16
+
17
+ export async function loader({context}: LoaderArgs) {
18
+ const customerAccessToken = await context.session.get('customerAccessToken');
19
+ if (customerAccessToken) {
20
+ return redirect('/account');
21
+ }
22
+
23
+ return json({});
24
+ }
25
+
26
+ export const action: ActionFunction = async ({request, context}) => {
27
+ if (request.method !== 'POST') {
28
+ return json({error: 'Method not allowed'}, {status: 405});
29
+ }
30
+
31
+ const {storefront, session} = context;
32
+ const form = await request.formData();
33
+ const email = String(form.has('email') ? form.get('email') : '');
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
+ const validInputs = Boolean(email && password);
43
+ try {
44
+ if (!validPasswords) {
45
+ throw new Error('Passwords do not match');
46
+ }
47
+
48
+ if (!validInputs) {
49
+ throw new Error('Please provide both an email and a password.');
50
+ }
51
+
52
+ const {customerCreate} = await storefront.mutate(CUSTOMER_CREATE_MUTATION, {
53
+ variables: {
54
+ input: {email, password},
55
+ },
56
+ });
57
+
58
+ if (customerCreate?.customerUserErrors?.length) {
59
+ throw new Error(customerCreate?.customerUserErrors[0].message);
60
+ }
61
+
62
+ const newCustomer = customerCreate?.customer;
63
+ if (!newCustomer?.id) {
64
+ throw new Error('Could not create customer');
65
+ }
66
+
67
+ // get an access token for the new customer
68
+ const {customerAccessTokenCreate} = await storefront.mutate(
69
+ REGISTER_LOGIN_MUTATION,
70
+ {
71
+ variables: {
72
+ input: {
73
+ email,
74
+ password,
75
+ },
76
+ },
77
+ },
78
+ );
79
+
80
+ if (!customerAccessTokenCreate?.customerAccessToken?.accessToken) {
81
+ throw new Error('Missing access token');
82
+ }
83
+ session.set(
84
+ 'customerAccessToken',
85
+ customerAccessTokenCreate?.customerAccessToken,
86
+ );
87
+
88
+ return json(
89
+ {error: null, newCustomer},
90
+ {
91
+ status: 302,
92
+ headers: {
93
+ 'Set-Cookie': await session.commit(),
94
+ Location: '/account',
95
+ },
96
+ },
97
+ );
98
+ } catch (error: unknown) {
99
+ if (error instanceof Error) {
100
+ return json({error: error.message}, {status: 400});
101
+ }
102
+ return json({error}, {status: 400});
103
+ }
104
+ };
105
+
106
+ export default function Register() {
107
+ const data = useActionData<ActionResponse>();
108
+ const error = data?.error || null;
109
+ return (
110
+ <div className="login">
111
+ <h1>Register.</h1>
112
+ <Form method="POST">
113
+ <fieldset>
114
+ <label htmlFor="email">Email address</label>
115
+ <input
116
+ id="email"
117
+ name="email"
118
+ type="email"
119
+ autoComplete="email"
120
+ required
121
+ placeholder="Email address"
122
+ aria-label="Email address"
123
+ // eslint-disable-next-line jsx-a11y/no-autofocus
124
+ autoFocus
125
+ />
126
+ <label htmlFor="password">Password</label>
127
+ <input
128
+ id="password"
129
+ name="password"
130
+ type="password"
131
+ autoComplete="current-password"
132
+ placeholder="Password"
133
+ aria-label="Password"
134
+ minLength={8}
135
+ required
136
+ />
137
+ <label htmlFor="passwordConfirm">Re-enter password</label>
138
+ <input
139
+ id="passwordConfirm"
140
+ name="passwordConfirm"
141
+ type="password"
142
+ autoComplete="current-password"
143
+ placeholder="Re-enter password"
144
+ aria-label="Re-enter password"
145
+ minLength={8}
146
+ required
147
+ />
148
+ </fieldset>
149
+ {error ? (
150
+ <p>
151
+ <mark>
152
+ <small>{error}</small>
153
+ </mark>
154
+ </p>
155
+ ) : (
156
+ <br />
157
+ )}
158
+ <button type="submit">Register</button>
159
+ </Form>
160
+ <br />
161
+ <p>
162
+ <Link to="/account/login">Login →</Link>
163
+ </p>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerCreate
169
+ const CUSTOMER_CREATE_MUTATION = `#graphql
170
+ mutation customerCreate(
171
+ $input: CustomerCreateInput!,
172
+ $country: CountryCode,
173
+ $language: LanguageCode
174
+ ) @inContext(country: $country, language: $language) {
175
+ customerCreate(input: $input) {
176
+ customer {
177
+ id
178
+ }
179
+ customerUserErrors {
180
+ code
181
+ field
182
+ message
183
+ }
184
+ }
185
+ }
186
+ ` as const;
187
+
188
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customeraccesstokencreate
189
+ const REGISTER_LOGIN_MUTATION = `#graphql
190
+ mutation registerLogin(
191
+ $input: CustomerAccessTokenCreateInput!,
192
+ $country: CountryCode,
193
+ $language: LanguageCode
194
+ ) @inContext(country: $country, language: $language) {
195
+ customerAccessTokenCreate(input: $input) {
196
+ customerUserErrors {
197
+ code
198
+ field
199
+ message
200
+ }
201
+ customerAccessToken {
202
+ accessToken
203
+ expiresAt
204
+ }
205
+ }
206
+ }
207
+ ` as const;
@@ -0,0 +1,136 @@
1
+ import {type ActionArgs, json, redirect} from '@shopify/remix-oxygen';
2
+ import {Form, useActionData, type V2_MetaFunction} from '@remix-run/react';
3
+
4
+ type ActionResponse = {
5
+ error: string | null;
6
+ };
7
+
8
+ export const meta: V2_MetaFunction = () => {
9
+ return [{title: 'Reset Password'}];
10
+ };
11
+
12
+ export async function action({request, context, params}: ActionArgs) {
13
+ if (request.method !== 'POST') {
14
+ return json({error: 'Method not allowed'}, {status: 405});
15
+ }
16
+ const {id, resetToken} = params;
17
+ const {session, storefront} = context;
18
+
19
+ try {
20
+ if (!id || !resetToken) {
21
+ throw new Error('customer token or id not found');
22
+ }
23
+
24
+ const form = await request.formData();
25
+ const password = form.has('password') ? String(form.get('password')) : '';
26
+ const passwordConfirm = form.has('passwordConfirm')
27
+ ? String(form.get('passwordConfirm'))
28
+ : '';
29
+ const validInputs = Boolean(password && passwordConfirm);
30
+ if (validInputs && password !== passwordConfirm) {
31
+ throw new Error('Please provide matching passwords');
32
+ }
33
+
34
+ const {customerReset} = await storefront.mutate(CUSTOMER_RESET_MUTATION, {
35
+ variables: {
36
+ id: `gid://shopify/Customer/${id}`,
37
+ input: {password, resetToken},
38
+ },
39
+ });
40
+
41
+ if (customerReset?.customerUserErrors?.length) {
42
+ throw new Error(customerReset?.customerUserErrors[0].message);
43
+ }
44
+
45
+ if (!customerReset?.customerAccessToken) {
46
+ throw new Error('Access token not found. Please try again.');
47
+ }
48
+ session.set('customerAccessToken', customerReset.customerAccessToken);
49
+
50
+ return redirect('/account', {
51
+ headers: {
52
+ 'Set-Cookie': await session.commit(),
53
+ },
54
+ });
55
+ } catch (error: unknown) {
56
+ if (error instanceof Error) {
57
+ return json({error: error.message}, {status: 400});
58
+ }
59
+ return json({error}, {status: 400});
60
+ }
61
+ }
62
+
63
+ export default function Reset() {
64
+ const action = useActionData<ActionResponse>();
65
+
66
+ return (
67
+ <div className="account-reset">
68
+ <h1>Reset Password.</h1>
69
+ <p>Enter a new password for your account.</p>
70
+ <Form method="POST">
71
+ <fieldset>
72
+ <label htmlFor="password">Password</label>
73
+ <input
74
+ aria-label="Password"
75
+ autoComplete="current-password"
76
+ // eslint-disable-next-line jsx-a11y/no-autofocus
77
+ autoFocus
78
+ id="password"
79
+ minLength={8}
80
+ name="password"
81
+ placeholder="Password"
82
+ required
83
+ type="password"
84
+ />
85
+ <label htmlFor="passwordConfirm">Re-enter password</label>
86
+ <input
87
+ aria-label="Re-enter password"
88
+ autoComplete="current-password"
89
+ id="passwordConfirm"
90
+ minLength={8}
91
+ name="passwordConfirm"
92
+ placeholder="Re-enter password"
93
+ required
94
+ type="password"
95
+ />
96
+ </fieldset>
97
+ {action?.error ? (
98
+ <p>
99
+ <mark>
100
+ <small>{action.error}</small>
101
+ </mark>
102
+ </p>
103
+ ) : (
104
+ <br />
105
+ )}
106
+ <button type="submit">Reset</button>
107
+ </Form>
108
+ <br />
109
+ <p>
110
+ <a href="/account/login">Back to login →</a>
111
+ </p>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerreset
117
+ const CUSTOMER_RESET_MUTATION = `#graphql
118
+ mutation customerReset(
119
+ $id: ID!,
120
+ $input: CustomerResetInput!
121
+ $country: CountryCode
122
+ $language: LanguageCode
123
+ ) @inContext(country: $country, language: $language) {
124
+ customerReset(id: $id, input: $input) {
125
+ customerAccessToken {
126
+ accessToken
127
+ expiresAt
128
+ }
129
+ customerUserErrors {
130
+ code
131
+ field
132
+ message
133
+ }
134
+ }
135
+ }
136
+ ` as const;