@shopify/cli-hydrogen 6.0.2 → 7.0.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.
- package/dist/commands/hydrogen/build.js +40 -78
- package/dist/commands/hydrogen/codegen.js +8 -3
- package/dist/commands/hydrogen/deploy.js +173 -37
- package/dist/commands/hydrogen/deploy.test.js +192 -20
- package/dist/commands/hydrogen/dev.js +56 -31
- package/dist/commands/hydrogen/init.js +1 -1
- package/dist/commands/hydrogen/init.test.js +155 -53
- package/dist/commands/hydrogen/link.js +5 -21
- package/dist/commands/hydrogen/link.test.js +10 -10
- package/dist/commands/hydrogen/preview.js +22 -11
- package/dist/commands/hydrogen/setup.js +0 -4
- package/dist/commands/hydrogen/setup.test.js +0 -1
- package/dist/commands/hydrogen/shortcut.js +1 -0
- package/dist/commands/hydrogen/upgrade.js +720 -0
- package/dist/commands/hydrogen/upgrade.test.js +786 -0
- package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
- package/dist/generator-templates/starter/CHANGELOG.md +126 -0
- package/dist/generator-templates/starter/README.md +23 -0
- package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
- package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
- package/dist/generator-templates/starter/app/components/Layout.tsx +14 -11
- package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
- package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
- package/dist/generator-templates/starter/app/lib/session.ts +67 -0
- package/dist/generator-templates/starter/app/root.tsx +11 -45
- package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
- package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
- package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
- package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
- package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
- package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
- package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
- package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
- package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
- package/dist/generator-templates/starter/package.json +11 -10
- package/dist/generator-templates/starter/remix.config.js +4 -0
- package/dist/generator-templates/starter/remix.env.d.ts +6 -11
- package/dist/generator-templates/starter/server.ts +24 -167
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
- package/dist/hooks/init.js +4 -4
- package/dist/lib/auth.js +5 -10
- package/dist/lib/build.js +6 -1
- package/dist/lib/bundle/analyzer.js +36 -26
- package/dist/lib/check-lockfile.js +1 -0
- package/dist/lib/codegen.js +59 -18
- package/dist/lib/defer.js +12 -0
- package/dist/lib/file.js +52 -3
- package/dist/lib/flags.js +27 -9
- package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
- package/dist/lib/graphql/admin/client.test.js +2 -2
- package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
- package/dist/lib/log.js +32 -14
- package/dist/lib/mini-oxygen/assets.js +118 -0
- package/dist/lib/mini-oxygen/common.js +2 -1
- package/dist/lib/mini-oxygen/index.js +7 -5
- package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
- package/dist/lib/mini-oxygen/node.js +19 -5
- package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
- package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
- package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
- package/dist/lib/mini-oxygen/workerd.js +74 -50
- package/dist/lib/missing-routes.js +6 -3
- package/dist/lib/onboarding/common.js +40 -9
- package/dist/lib/onboarding/local.js +19 -11
- package/dist/lib/onboarding/remote.js +48 -28
- package/dist/lib/render-errors.js +2 -0
- package/dist/lib/request-events.js +65 -31
- package/dist/lib/setups/css/assets.js +1 -46
- package/dist/lib/setups/css/css-modules.js +3 -2
- package/dist/lib/setups/css/postcss.js +4 -2
- package/dist/lib/setups/css/tailwind.js +4 -2
- package/dist/lib/setups/css/vanilla-extract.js +3 -2
- package/dist/lib/setups/i18n/replacers.test.js +56 -38
- package/dist/lib/shell.js +1 -1
- package/dist/lib/template-diff.js +89 -0
- package/dist/lib/template-downloader.js +3 -2
- package/dist/lib/transpile/project.js +1 -1
- package/dist/virtual-routes/assets/debug-network.css +592 -0
- package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
- package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
- package/dist/virtual-routes/components/IconClose.jsx +38 -0
- package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
- package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
- package/dist/virtual-routes/components/RequestTable.jsx +92 -0
- package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
- package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
- package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
- package/oclif.manifest.json +134 -59
- package/package.json +18 -26
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
- package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
- package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
- package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
- package/dist/virtual-routes/routes/debug-network.jsx +0 -289
- /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {CustomerFragment} from '
|
|
2
|
-
import type {CustomerUpdateInput} from '@shopify/hydrogen/
|
|
1
|
+
import type {CustomerFragment} from 'customer-accountapi.generated';
|
|
2
|
+
import type {CustomerUpdateInput} from '@shopify/hydrogen/customer-account-api-types';
|
|
3
|
+
import {CUSTOMER_UPDATE_MUTATION} from '~/graphql/customer-account/CustomerUpdateMutation';
|
|
3
4
|
import {
|
|
4
5
|
json,
|
|
5
6
|
redirect,
|
|
@@ -24,86 +25,78 @@ export const meta: MetaFunction = () => {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export async function loader({context}: LoaderFunctionArgs) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
await context.customerAccount.handleAuthStatus();
|
|
29
|
+
|
|
30
|
+
return json(
|
|
31
|
+
{},
|
|
32
|
+
{
|
|
33
|
+
headers: {
|
|
34
|
+
'Set-Cookie': await context.session.commit(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export async function action({request, context}: ActionFunctionArgs) {
|
|
35
|
-
const {
|
|
41
|
+
const {customerAccount} = context;
|
|
36
42
|
|
|
37
43
|
if (request.method !== 'PUT') {
|
|
38
44
|
return json({error: 'Method not allowed'}, {status: 405});
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const form = await request.formData();
|
|
42
|
-
const customerAccessToken = await session.get('customerAccessToken');
|
|
43
|
-
if (!customerAccessToken) {
|
|
44
|
-
return json({error: 'Unauthorized'}, {status: 401});
|
|
45
|
-
}
|
|
46
48
|
|
|
47
49
|
try {
|
|
48
|
-
const password = getPassword(form);
|
|
49
50
|
const customer: CustomerUpdateInput = {};
|
|
50
|
-
const validInputKeys = [
|
|
51
|
-
'firstName',
|
|
52
|
-
'lastName',
|
|
53
|
-
'email',
|
|
54
|
-
'password',
|
|
55
|
-
'phone',
|
|
56
|
-
] as const;
|
|
51
|
+
const validInputKeys = ['firstName', 'lastName'] as const;
|
|
57
52
|
for (const [key, value] of form.entries()) {
|
|
58
53
|
if (!validInputKeys.includes(key as any)) {
|
|
59
54
|
continue;
|
|
60
55
|
}
|
|
61
|
-
if (key === 'acceptsMarketing') {
|
|
62
|
-
customer.acceptsMarketing = value === 'on';
|
|
63
|
-
}
|
|
64
56
|
if (typeof value === 'string' && value.length) {
|
|
65
57
|
customer[key as (typeof validInputKeys)[number]] = value;
|
|
66
58
|
}
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
if (password) {
|
|
70
|
-
customer.password = password;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
61
|
// update customer and possibly password
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
const {data, errors} = await customerAccount.mutate(
|
|
63
|
+
CUSTOMER_UPDATE_MUTATION,
|
|
64
|
+
{
|
|
65
|
+
variables: {
|
|
66
|
+
customer,
|
|
67
|
+
},
|
|
78
68
|
},
|
|
79
|
-
|
|
69
|
+
);
|
|
80
70
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return json(
|
|
84
|
-
{error: updated.customerUpdate?.customerUserErrors[0]},
|
|
85
|
-
{status: 400},
|
|
86
|
-
);
|
|
71
|
+
if (errors?.length) {
|
|
72
|
+
throw new Error(errors[0].message);
|
|
87
73
|
}
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
session.set(
|
|
92
|
-
'customerAccessToken',
|
|
93
|
-
updated.customerUpdate?.customerAccessToken,
|
|
94
|
-
);
|
|
75
|
+
if (!data?.customerUpdate?.customer) {
|
|
76
|
+
throw new Error('Customer profile update failed.');
|
|
95
77
|
}
|
|
96
78
|
|
|
97
79
|
return json(
|
|
98
|
-
{
|
|
80
|
+
{
|
|
81
|
+
error: null,
|
|
82
|
+
customer: data?.customerUpdate?.customer,
|
|
83
|
+
},
|
|
99
84
|
{
|
|
100
85
|
headers: {
|
|
101
|
-
'Set-Cookie': await session.commit(),
|
|
86
|
+
'Set-Cookie': await context.session.commit(),
|
|
102
87
|
},
|
|
103
88
|
},
|
|
104
89
|
);
|
|
105
90
|
} catch (error: any) {
|
|
106
|
-
return json(
|
|
91
|
+
return json(
|
|
92
|
+
{error: error.message, customer: null},
|
|
93
|
+
{
|
|
94
|
+
status: 400,
|
|
95
|
+
headers: {
|
|
96
|
+
'Set-Cookie': await context.session.commit(),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
);
|
|
107
100
|
}
|
|
108
101
|
}
|
|
109
102
|
|
|
@@ -142,75 +135,6 @@ export default function AccountProfile() {
|
|
|
142
135
|
defaultValue={customer.lastName ?? ''}
|
|
143
136
|
minLength={2}
|
|
144
137
|
/>
|
|
145
|
-
<label htmlFor="phone">Mobile</label>
|
|
146
|
-
<input
|
|
147
|
-
id="phone"
|
|
148
|
-
name="phone"
|
|
149
|
-
type="tel"
|
|
150
|
-
autoComplete="tel"
|
|
151
|
-
placeholder="Mobile"
|
|
152
|
-
aria-label="Mobile"
|
|
153
|
-
defaultValue={customer.phone ?? ''}
|
|
154
|
-
/>
|
|
155
|
-
<label htmlFor="email">Email address</label>
|
|
156
|
-
<input
|
|
157
|
-
id="email"
|
|
158
|
-
name="email"
|
|
159
|
-
type="email"
|
|
160
|
-
autoComplete="email"
|
|
161
|
-
required
|
|
162
|
-
placeholder="Email address"
|
|
163
|
-
aria-label="Email address"
|
|
164
|
-
defaultValue={customer.email ?? ''}
|
|
165
|
-
/>
|
|
166
|
-
<div className="account-profile-marketing">
|
|
167
|
-
<input
|
|
168
|
-
id="acceptsMarketing"
|
|
169
|
-
name="acceptsMarketing"
|
|
170
|
-
type="checkbox"
|
|
171
|
-
placeholder="Accept marketing"
|
|
172
|
-
aria-label="Accept marketing"
|
|
173
|
-
defaultChecked={customer.acceptsMarketing}
|
|
174
|
-
/>
|
|
175
|
-
<label htmlFor="acceptsMarketing">
|
|
176
|
-
Subscribed to marketing communications
|
|
177
|
-
</label>
|
|
178
|
-
</div>
|
|
179
|
-
</fieldset>
|
|
180
|
-
<br />
|
|
181
|
-
<legend>Change password (optional)</legend>
|
|
182
|
-
<fieldset>
|
|
183
|
-
<label htmlFor="currentPassword">Current password</label>
|
|
184
|
-
<input
|
|
185
|
-
id="currentPassword"
|
|
186
|
-
name="currentPassword"
|
|
187
|
-
type="password"
|
|
188
|
-
autoComplete="current-password"
|
|
189
|
-
placeholder="Current password"
|
|
190
|
-
aria-label="Current password"
|
|
191
|
-
minLength={8}
|
|
192
|
-
/>
|
|
193
|
-
|
|
194
|
-
<label htmlFor="newPassword">New password</label>
|
|
195
|
-
<input
|
|
196
|
-
id="newPassword"
|
|
197
|
-
name="newPassword"
|
|
198
|
-
type="password"
|
|
199
|
-
placeholder="New password"
|
|
200
|
-
aria-label="New password"
|
|
201
|
-
minLength={8}
|
|
202
|
-
/>
|
|
203
|
-
|
|
204
|
-
<label htmlFor="newPasswordConfirm">New password (confirm)</label>
|
|
205
|
-
<input
|
|
206
|
-
id="newPasswordConfirm"
|
|
207
|
-
name="newPasswordConfirm"
|
|
208
|
-
type="password"
|
|
209
|
-
placeholder="New password (confirm)"
|
|
210
|
-
aria-label="New password confirm"
|
|
211
|
-
minLength={8}
|
|
212
|
-
/>
|
|
213
|
-
<small>Passwords must be at least 8 characters.</small>
|
|
214
138
|
</fieldset>
|
|
215
139
|
{action?.error ? (
|
|
216
140
|
<p>
|
|
@@ -228,67 +152,3 @@ export default function AccountProfile() {
|
|
|
228
152
|
</div>
|
|
229
153
|
);
|
|
230
154
|
}
|
|
231
|
-
|
|
232
|
-
function getPassword(form: FormData): string | undefined {
|
|
233
|
-
let password;
|
|
234
|
-
const currentPassword = form.get('currentPassword');
|
|
235
|
-
const newPassword = form.get('newPassword');
|
|
236
|
-
const newPasswordConfirm = form.get('newPasswordConfirm');
|
|
237
|
-
|
|
238
|
-
let passwordError;
|
|
239
|
-
if (newPassword && !currentPassword) {
|
|
240
|
-
passwordError = new Error('Current password is required.');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (newPassword && newPassword !== newPasswordConfirm) {
|
|
244
|
-
passwordError = new Error('New passwords must match.');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (newPassword && currentPassword && newPassword === currentPassword) {
|
|
248
|
-
passwordError = new Error(
|
|
249
|
-
'New password must be different than current password.',
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (passwordError) {
|
|
254
|
-
throw passwordError;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (currentPassword && newPassword) {
|
|
258
|
-
password = newPassword;
|
|
259
|
-
} else {
|
|
260
|
-
password = currentPassword;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return String(password);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const CUSTOMER_UPDATE_MUTATION = `#graphql
|
|
267
|
-
# https://shopify.dev/docs/api/storefront/latest/mutations/customerUpdate
|
|
268
|
-
mutation customerUpdate(
|
|
269
|
-
$customerAccessToken: String!,
|
|
270
|
-
$customer: CustomerUpdateInput!
|
|
271
|
-
$country: CountryCode
|
|
272
|
-
$language: LanguageCode
|
|
273
|
-
) @inContext(language: $language, country: $country) {
|
|
274
|
-
customerUpdate(customerAccessToken: $customerAccessToken, customer: $customer) {
|
|
275
|
-
customer {
|
|
276
|
-
acceptsMarketing
|
|
277
|
-
email
|
|
278
|
-
firstName
|
|
279
|
-
id
|
|
280
|
-
lastName
|
|
281
|
-
phone
|
|
282
|
-
}
|
|
283
|
-
customerAccessToken {
|
|
284
|
-
accessToken
|
|
285
|
-
expiresAt
|
|
286
|
-
}
|
|
287
|
-
customerUserErrors {
|
|
288
|
-
code
|
|
289
|
-
field
|
|
290
|
-
message
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
` as const;
|
|
@@ -1,104 +1,34 @@
|
|
|
1
|
-
import {Form, NavLink, Outlet, useLoaderData} from '@remix-run/react';
|
|
2
1
|
import {json, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
3
|
-
import
|
|
2
|
+
import {Form, NavLink, Outlet, useLoaderData} from '@remix-run/react';
|
|
3
|
+
import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery';
|
|
4
4
|
|
|
5
5
|
export function shouldRevalidate() {
|
|
6
6
|
return true;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export async function loader({
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const isLoggedIn = !!customerAccessToken?.accessToken;
|
|
14
|
-
const isAccountHome = pathname === '/account' || pathname === '/account/';
|
|
15
|
-
const isPrivateRoute =
|
|
16
|
-
/^\/account\/(orders|orders\/.*|profile|addresses|addresses\/.*)$/.test(
|
|
17
|
-
pathname,
|
|
18
|
-
);
|
|
9
|
+
export async function loader({context}: LoaderFunctionArgs) {
|
|
10
|
+
const {data, errors} = await context.customerAccount.query(
|
|
11
|
+
CUSTOMER_DETAILS_QUERY,
|
|
12
|
+
);
|
|
19
13
|
|
|
20
|
-
if (!
|
|
21
|
-
|
|
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
|
-
}
|
|
14
|
+
if (errors?.length || !data?.customer) {
|
|
15
|
+
throw new Error('Customer not found');
|
|
42
16
|
}
|
|
43
17
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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', {
|
|
18
|
+
return json(
|
|
19
|
+
{customer: data.customer},
|
|
20
|
+
{
|
|
71
21
|
headers: {
|
|
72
|
-
'
|
|
22
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
23
|
+
'Set-Cookie': await context.session.commit(),
|
|
73
24
|
},
|
|
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>
|
|
25
|
+
},
|
|
92
26
|
);
|
|
93
27
|
}
|
|
94
28
|
|
|
95
|
-
function AccountLayout({
|
|
96
|
-
customer
|
|
97
|
-
|
|
98
|
-
}: {
|
|
99
|
-
customer: CustomerFragment;
|
|
100
|
-
children: React.ReactNode;
|
|
101
|
-
}) {
|
|
29
|
+
export default function AccountLayout() {
|
|
30
|
+
const {customer} = useLoaderData<typeof loader>();
|
|
31
|
+
|
|
102
32
|
const heading = customer
|
|
103
33
|
? customer.firstName
|
|
104
34
|
? `Welcome, ${customer.firstName}`
|
|
@@ -110,7 +40,9 @@ function AccountLayout({
|
|
|
110
40
|
<h1>{heading}</h1>
|
|
111
41
|
<br />
|
|
112
42
|
<AccountMenu />
|
|
113
|
-
|
|
43
|
+
<br />
|
|
44
|
+
<br />
|
|
45
|
+
<Outlet context={{customer}} />
|
|
114
46
|
</div>
|
|
115
47
|
);
|
|
116
48
|
}
|
|
@@ -155,50 +87,3 @@ function Logout() {
|
|
|
155
87
|
</Form>
|
|
156
88
|
);
|
|
157
89
|
}
|
|
158
|
-
|
|
159
|
-
export const CUSTOMER_FRAGMENT = `#graphql
|
|
160
|
-
fragment Customer on Customer {
|
|
161
|
-
acceptsMarketing
|
|
162
|
-
addresses(first: 6) {
|
|
163
|
-
nodes {
|
|
164
|
-
...Address
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
defaultAddress {
|
|
168
|
-
...Address
|
|
169
|
-
}
|
|
170
|
-
email
|
|
171
|
-
firstName
|
|
172
|
-
lastName
|
|
173
|
-
numberOfOrders
|
|
174
|
-
phone
|
|
175
|
-
}
|
|
176
|
-
fragment Address on MailingAddress {
|
|
177
|
-
id
|
|
178
|
-
formatted
|
|
179
|
-
firstName
|
|
180
|
-
lastName
|
|
181
|
-
company
|
|
182
|
-
address1
|
|
183
|
-
address2
|
|
184
|
-
country
|
|
185
|
-
province
|
|
186
|
-
city
|
|
187
|
-
zip
|
|
188
|
-
phone
|
|
189
|
-
}
|
|
190
|
-
` as const;
|
|
191
|
-
|
|
192
|
-
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/customer
|
|
193
|
-
const CUSTOMER_QUERY = `#graphql
|
|
194
|
-
query Customer(
|
|
195
|
-
$customerAccessToken: String!
|
|
196
|
-
$country: CountryCode
|
|
197
|
-
$language: LanguageCode
|
|
198
|
-
) @inContext(country: $country, language: $language) {
|
|
199
|
-
customer(customerAccessToken: $customerAccessToken) {
|
|
200
|
-
...Customer
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
${CUSTOMER_FRAGMENT}
|
|
204
|
-
` as const;
|
|
@@ -1,142 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
json,
|
|
3
|
-
redirect,
|
|
4
|
-
type ActionFunctionArgs,
|
|
5
|
-
type LoaderFunctionArgs,
|
|
6
|
-
} from '@shopify/remix-oxygen';
|
|
7
|
-
import {Form, Link, useActionData, type MetaFunction} from '@remix-run/react';
|
|
1
|
+
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const meta: MetaFunction = () => {
|
|
14
|
-
return [{title: 'Login'}];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export async function loader({context}: LoaderFunctionArgs) {
|
|
18
|
-
if (await context.session.get('customerAccessToken')) {
|
|
19
|
-
return redirect('/account');
|
|
20
|
-
}
|
|
21
|
-
return json({});
|
|
3
|
+
export async function loader({request, context}: LoaderFunctionArgs) {
|
|
4
|
+
return context.customerAccount.login();
|
|
22
5
|
}
|
|
23
|
-
|
|
24
|
-
export async function action({request, context}: ActionFunctionArgs) {
|
|
25
|
-
const {session, storefront} = context;
|
|
26
|
-
|
|
27
|
-
if (request.method !== 'POST') {
|
|
28
|
-
return json({error: 'Method not allowed'}, {status: 405});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const form = await request.formData();
|
|
33
|
-
const email = String(form.has('email') ? form.get('email') : '');
|
|
34
|
-
const password = String(form.has('password') ? form.get('password') : '');
|
|
35
|
-
const validInputs = Boolean(email && password);
|
|
36
|
-
|
|
37
|
-
if (!validInputs) {
|
|
38
|
-
throw new Error('Please provide both an email and a password.');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const {customerAccessTokenCreate} = await storefront.mutate(
|
|
42
|
-
LOGIN_MUTATION,
|
|
43
|
-
{
|
|
44
|
-
variables: {
|
|
45
|
-
input: {email, password},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
if (!customerAccessTokenCreate?.customerAccessToken?.accessToken) {
|
|
51
|
-
throw new Error(customerAccessTokenCreate?.customerUserErrors[0].message);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const {customerAccessToken} = customerAccessTokenCreate;
|
|
55
|
-
session.set('customerAccessToken', customerAccessToken);
|
|
56
|
-
|
|
57
|
-
return redirect('/account', {
|
|
58
|
-
headers: {
|
|
59
|
-
'Set-Cookie': await session.commit(),
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
} catch (error: unknown) {
|
|
63
|
-
if (error instanceof Error) {
|
|
64
|
-
return json({error: error.message}, {status: 400});
|
|
65
|
-
}
|
|
66
|
-
return json({error}, {status: 400});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default function Login() {
|
|
71
|
-
const data = useActionData<ActionResponse>();
|
|
72
|
-
const error = data?.error || null;
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div className="login">
|
|
76
|
-
<h1>Sign in.</h1>
|
|
77
|
-
<Form method="POST">
|
|
78
|
-
<fieldset>
|
|
79
|
-
<label htmlFor="email">Email address</label>
|
|
80
|
-
<input
|
|
81
|
-
id="email"
|
|
82
|
-
name="email"
|
|
83
|
-
type="email"
|
|
84
|
-
autoComplete="email"
|
|
85
|
-
required
|
|
86
|
-
placeholder="Email address"
|
|
87
|
-
aria-label="Email address"
|
|
88
|
-
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
89
|
-
autoFocus
|
|
90
|
-
/>
|
|
91
|
-
<label htmlFor="password">Password</label>
|
|
92
|
-
<input
|
|
93
|
-
id="password"
|
|
94
|
-
name="password"
|
|
95
|
-
type="password"
|
|
96
|
-
autoComplete="current-password"
|
|
97
|
-
placeholder="Password"
|
|
98
|
-
aria-label="Password"
|
|
99
|
-
minLength={8}
|
|
100
|
-
required
|
|
101
|
-
/>
|
|
102
|
-
</fieldset>
|
|
103
|
-
{error ? (
|
|
104
|
-
<p>
|
|
105
|
-
<mark>
|
|
106
|
-
<small>{error}</small>
|
|
107
|
-
</mark>
|
|
108
|
-
</p>
|
|
109
|
-
) : (
|
|
110
|
-
<br />
|
|
111
|
-
)}
|
|
112
|
-
<button type="submit">Sign in</button>
|
|
113
|
-
</Form>
|
|
114
|
-
<br />
|
|
115
|
-
<div>
|
|
116
|
-
<p>
|
|
117
|
-
<Link to="/account/recover">Forgot password →</Link>
|
|
118
|
-
</p>
|
|
119
|
-
<p>
|
|
120
|
-
<Link to="/account/register">Register →</Link>
|
|
121
|
-
</p>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customeraccesstokencreate
|
|
128
|
-
const LOGIN_MUTATION = `#graphql
|
|
129
|
-
mutation login($input: CustomerAccessTokenCreateInput!) {
|
|
130
|
-
customerAccessTokenCreate(input: $input) {
|
|
131
|
-
customerUserErrors {
|
|
132
|
-
code
|
|
133
|
-
field
|
|
134
|
-
message
|
|
135
|
-
}
|
|
136
|
-
customerAccessToken {
|
|
137
|
-
accessToken
|
|
138
|
-
expiresAt
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
` as const;
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {type MetaFunction} from '@remix-run/react';
|
|
3
|
-
|
|
4
|
-
export const meta: MetaFunction = () => {
|
|
5
|
-
return [{title: 'Logout'}];
|
|
6
|
-
};
|
|
1
|
+
import {redirect, type ActionFunctionArgs} from '@shopify/remix-oxygen';
|
|
7
2
|
|
|
3
|
+
// if we dont implement this, /account/logout will get caught by account.$.tsx to do login
|
|
8
4
|
export async function loader() {
|
|
9
|
-
return redirect('/
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function action({request, context}: ActionFunctionArgs) {
|
|
13
|
-
const {session} = context;
|
|
14
|
-
session.unset('customerAccessToken');
|
|
15
|
-
|
|
16
|
-
if (request.method !== 'POST') {
|
|
17
|
-
return json({error: 'Method not allowed'}, {status: 405});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return redirect('/', {
|
|
21
|
-
headers: {
|
|
22
|
-
'Set-Cookie': await session.commit(),
|
|
23
|
-
},
|
|
24
|
-
});
|
|
5
|
+
return redirect('/');
|
|
25
6
|
}
|
|
26
7
|
|
|
27
|
-
export
|
|
28
|
-
return
|
|
8
|
+
export async function action({context}: ActionFunctionArgs) {
|
|
9
|
+
return context.customerAccount.logout();
|
|
29
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Await, type MetaFunction} from '@remix-run/react';
|
|
2
2
|
import {Suspense} from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type {CartQueryDataReturn} from '@shopify/hydrogen';
|
|
4
4
|
import {CartForm} from '@shopify/hydrogen';
|
|
5
5
|
import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
|
|
6
6
|
import {CartMain} from '~/components/Cart';
|
|
@@ -11,11 +11,11 @@ export const meta: MetaFunction = () => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export async function action({request, context}: ActionFunctionArgs) {
|
|
14
|
-
const {
|
|
14
|
+
const {cart} = context;
|
|
15
15
|
|
|
16
16
|
const [formData, customerAccessToken] = await Promise.all([
|
|
17
17
|
request.formData(),
|
|
18
|
-
|
|
18
|
+
await context.customerAccount.getAccessToken(),
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
21
|
const {action, inputs} = CartForm.getFormInput(formData);
|
|
@@ -25,7 +25,7 @@ export async function action({request, context}: ActionFunctionArgs) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let status = 200;
|
|
28
|
-
let result:
|
|
28
|
+
let result: CartQueryDataReturn;
|
|
29
29
|
|
|
30
30
|
switch (action) {
|
|
31
31
|
case CartForm.ACTIONS.LinesAdd:
|
|
@@ -54,7 +54,7 @@ export async function action({request, context}: ActionFunctionArgs) {
|
|
|
54
54
|
case CartForm.ACTIONS.BuyerIdentityUpdate: {
|
|
55
55
|
result = await cart.updateBuyerIdentity({
|
|
56
56
|
...inputs.buyerIdentity,
|
|
57
|
-
customerAccessToken
|
|
57
|
+
customerAccessToken,
|
|
58
58
|
});
|
|
59
59
|
break;
|
|
60
60
|
}
|
|
@@ -72,6 +72,8 @@ export async function action({request, context}: ActionFunctionArgs) {
|
|
|
72
72
|
headers.set('Location', redirectTo);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
headers.append('Set-Cookie', await context.session.commit());
|
|
76
|
+
|
|
75
77
|
return json(
|
|
76
78
|
{
|
|
77
79
|
cart: cartResult,
|