@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.
- package/dist/commands/hydrogen/build.js +16 -2
- package/dist/commands/hydrogen/codegen-unstable.js +13 -24
- package/dist/commands/hydrogen/dev.js +45 -39
- package/dist/commands/hydrogen/env/list.js +25 -24
- package/dist/commands/hydrogen/env/list.test.js +46 -43
- package/dist/commands/hydrogen/env/pull.js +53 -25
- package/dist/commands/hydrogen/env/pull.test.js +123 -42
- package/dist/commands/hydrogen/generate/route.js +31 -132
- package/dist/commands/hydrogen/generate/route.test.js +34 -126
- package/dist/commands/hydrogen/init.js +46 -127
- package/dist/commands/hydrogen/init.test.js +352 -100
- package/dist/commands/hydrogen/link.js +70 -69
- package/dist/commands/hydrogen/link.test.js +72 -107
- package/dist/commands/hydrogen/list.js +22 -12
- package/dist/commands/hydrogen/list.test.js +51 -48
- package/dist/commands/hydrogen/login.js +31 -0
- package/dist/commands/hydrogen/logout.js +21 -0
- package/dist/commands/hydrogen/setup/css.js +79 -0
- package/dist/commands/hydrogen/setup/markets.js +53 -0
- package/dist/commands/hydrogen/setup.js +133 -0
- package/dist/commands/hydrogen/shortcut.js +2 -45
- package/dist/commands/hydrogen/shortcut.test.js +10 -37
- package/dist/generator-templates/assets/css-modules/package.json +6 -0
- package/dist/generator-templates/assets/postcss/package.json +10 -0
- package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
- package/dist/generator-templates/assets/tailwind/package.json +13 -0
- package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
- package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
- package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
- package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
- package/dist/generator-templates/starter/.eslintignore +5 -0
- package/dist/generator-templates/starter/.eslintrc.js +18 -0
- package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
- package/dist/generator-templates/starter/README.md +40 -0
- package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
- package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
- package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
- package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
- package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
- package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
- package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
- package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
- package/dist/generator-templates/starter/app/root.tsx +270 -0
- package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
- package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
- package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
- package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
- package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
- package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
- package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
- package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
- package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
- package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
- package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
- package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
- package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
- package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
- package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
- package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
- package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
- package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
- package/dist/generator-templates/starter/app/styles/app.css +473 -0
- package/dist/generator-templates/starter/app/styles/reset.css +129 -0
- package/dist/generator-templates/starter/app/utils.ts +46 -0
- package/dist/generator-templates/starter/package.json +43 -0
- package/dist/generator-templates/starter/public/favicon.svg +28 -0
- package/dist/generator-templates/starter/remix.config.js +26 -0
- package/dist/generator-templates/starter/remix.env.d.ts +39 -0
- package/dist/generator-templates/starter/server.ts +253 -0
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
- package/dist/generator-templates/starter/tsconfig.json +22 -0
- package/dist/lib/auth.js +123 -0
- package/dist/lib/auth.test.js +157 -0
- package/dist/lib/build.js +51 -0
- package/dist/lib/check-version.js +3 -3
- package/dist/lib/check-version.test.js +24 -0
- package/dist/lib/codegen.js +26 -17
- package/dist/lib/environment-variables.js +68 -0
- package/dist/lib/environment-variables.test.js +147 -0
- package/dist/lib/file.js +41 -0
- package/dist/lib/file.test.js +69 -0
- package/dist/lib/flags.js +39 -2
- package/dist/lib/format-code.js +26 -0
- package/dist/lib/gid.js +12 -0
- package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
- package/dist/lib/graphql/admin/client.js +27 -0
- package/dist/lib/graphql/admin/client.test.js +51 -0
- package/dist/lib/graphql/admin/create-storefront.js +13 -15
- package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
- package/dist/lib/graphql/admin/fetch-job.js +6 -15
- package/dist/lib/graphql/admin/link-storefront.js +7 -11
- package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
- package/dist/lib/graphql/admin/list-environments.js +2 -2
- package/dist/lib/graphql/admin/list-environments.test.js +44 -0
- package/dist/lib/graphql/admin/list-storefronts.js +7 -11
- package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
- package/dist/lib/graphql/admin/pull-variables.js +3 -3
- package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
- package/dist/lib/graphql/business-platform/user-account.js +83 -0
- package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
- package/dist/lib/log.js +185 -9
- package/dist/lib/log.test.js +92 -0
- package/dist/lib/mini-oxygen.js +19 -9
- package/dist/lib/missing-routes.js +0 -2
- package/dist/lib/onboarding/common.js +456 -0
- package/dist/lib/onboarding/index.js +2 -0
- package/dist/lib/onboarding/local.js +229 -0
- package/dist/lib/onboarding/remote.js +89 -0
- package/dist/lib/remix-version-interop.js +5 -5
- package/dist/lib/remix-version-interop.test.js +11 -1
- package/dist/lib/render-errors.js +13 -11
- package/dist/lib/setups/css/assets.js +89 -0
- package/dist/lib/setups/css/css-modules.js +22 -0
- package/dist/lib/setups/css/index.js +44 -0
- package/dist/lib/setups/css/postcss.js +34 -0
- package/dist/lib/setups/css/replacers.js +137 -0
- package/dist/lib/setups/css/tailwind.js +54 -0
- package/dist/lib/setups/css/vanilla-extract.js +22 -0
- package/dist/lib/setups/i18n/domains.test.js +25 -0
- package/dist/lib/setups/i18n/index.js +46 -0
- package/dist/lib/setups/i18n/replacers.js +227 -0
- package/dist/lib/setups/i18n/subdomains.test.js +25 -0
- package/dist/lib/setups/i18n/subfolders.test.js +25 -0
- package/dist/lib/setups/i18n/templates/domains.js +14 -0
- package/dist/lib/setups/i18n/templates/domains.ts +25 -0
- package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
- package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
- package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
- package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
- package/dist/lib/setups/routes/generate.js +244 -0
- package/dist/lib/setups/routes/generate.test.js +313 -0
- package/dist/lib/shell.js +52 -5
- package/dist/lib/shell.test.js +42 -16
- package/dist/lib/shopify-config.js +23 -18
- package/dist/lib/shopify-config.test.js +63 -73
- package/dist/lib/template-downloader.js +9 -7
- package/dist/lib/transpile-ts.js +9 -29
- package/dist/virtual-routes/routes/index.jsx +40 -19
- package/oclif.manifest.json +710 -1
- package/package.json +16 -16
- package/dist/commands/hydrogen/build.d.ts +0 -23
- package/dist/commands/hydrogen/check.d.ts +0 -15
- package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
- package/dist/commands/hydrogen/dev.d.ts +0 -21
- package/dist/commands/hydrogen/env/list.d.ts +0 -18
- package/dist/commands/hydrogen/env/pull.d.ts +0 -22
- package/dist/commands/hydrogen/g.d.ts +0 -10
- package/dist/commands/hydrogen/generate/route.d.ts +0 -32
- package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
- package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
- package/dist/commands/hydrogen/init.d.ts +0 -24
- package/dist/commands/hydrogen/init.test.d.ts +0 -1
- package/dist/commands/hydrogen/link.d.ts +0 -23
- package/dist/commands/hydrogen/link.test.d.ts +0 -1
- package/dist/commands/hydrogen/list.d.ts +0 -21
- package/dist/commands/hydrogen/list.test.d.ts +0 -1
- package/dist/commands/hydrogen/preview.d.ts +0 -17
- package/dist/commands/hydrogen/shortcut.d.ts +0 -9
- package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
- package/dist/commands/hydrogen/unlink.d.ts +0 -16
- package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
- package/dist/create-app.d.ts +0 -1
- package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
- package/dist/generator-templates/routes/account/login.tsx +0 -103
- package/dist/generator-templates/routes/account/register.tsx +0 -103
- package/dist/generator-templates/routes/cart.tsx +0 -81
- package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
- package/dist/generator-templates/routes/collections/index.tsx +0 -102
- package/dist/generator-templates/routes/graphiql.tsx +0 -10
- package/dist/generator-templates/routes/index.tsx +0 -40
- package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
- package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
- package/dist/generator-templates/routes/policies/index.tsx +0 -117
- package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
- package/dist/hooks/init.d.ts +0 -5
- package/dist/lib/admin-session.d.ts +0 -6
- package/dist/lib/admin-session.js +0 -16
- package/dist/lib/admin-session.test.d.ts +0 -1
- package/dist/lib/admin-session.test.js +0 -27
- package/dist/lib/admin-urls.d.ts +0 -8
- package/dist/lib/check-lockfile.d.ts +0 -3
- package/dist/lib/check-lockfile.test.d.ts +0 -1
- package/dist/lib/check-version.d.ts +0 -16
- package/dist/lib/check-version.test.d.ts +0 -1
- package/dist/lib/codegen.d.ts +0 -26
- package/dist/lib/combined-environment-variables.d.ts +0 -8
- package/dist/lib/combined-environment-variables.js +0 -57
- package/dist/lib/combined-environment-variables.test.d.ts +0 -1
- package/dist/lib/combined-environment-variables.test.js +0 -111
- package/dist/lib/config.d.ts +0 -20
- package/dist/lib/flags.d.ts +0 -27
- package/dist/lib/flags.test.d.ts +0 -1
- package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
- package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
- package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
- package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
- package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
- package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
- package/dist/lib/graphql.d.ts +0 -21
- package/dist/lib/graphql.js +0 -18
- package/dist/lib/graphql.test.d.ts +0 -1
- package/dist/lib/log.d.ts +0 -6
- package/dist/lib/mini-oxygen.d.ts +0 -22
- package/dist/lib/missing-routes.d.ts +0 -8
- package/dist/lib/missing-routes.test.d.ts +0 -1
- package/dist/lib/missing-storefronts.d.ts +0 -5
- package/dist/lib/missing-storefronts.js +0 -18
- package/dist/lib/process.d.ts +0 -6
- package/dist/lib/pull-environment-variables.d.ts +0 -20
- package/dist/lib/pull-environment-variables.js +0 -57
- package/dist/lib/pull-environment-variables.test.d.ts +0 -1
- package/dist/lib/pull-environment-variables.test.js +0 -174
- package/dist/lib/remix-version-interop.d.ts +0 -11
- package/dist/lib/remix-version-interop.test.d.ts +0 -1
- package/dist/lib/render-errors.d.ts +0 -16
- package/dist/lib/shell.d.ts +0 -11
- package/dist/lib/shell.test.d.ts +0 -1
- package/dist/lib/shop.d.ts +0 -7
- package/dist/lib/shop.js +0 -32
- package/dist/lib/shop.test.d.ts +0 -1
- package/dist/lib/shop.test.js +0 -78
- package/dist/lib/shopify-config.d.ts +0 -35
- package/dist/lib/shopify-config.test.d.ts +0 -1
- package/dist/lib/string.d.ts +0 -3
- package/dist/lib/string.test.d.ts +0 -1
- package/dist/lib/template-downloader.d.ts +0 -6
- package/dist/lib/transpile-ts.d.ts +0 -16
- package/dist/lib/user-errors.d.ts +0 -9
- package/dist/lib/user-errors.js +0 -11
- package/dist/lib/virtual-routes.d.ts +0 -7
- package/dist/lib/virtual-routes.test.d.ts +0 -1
- /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
- /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
|
+
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
|
|
135
|
+
</NavLink>
|
|
136
|
+
|
|
|
137
|
+
<NavLink to="/account/profile" style={isActiveStyle}>
|
|
138
|
+
Profile
|
|
139
|
+
</NavLink>
|
|
140
|
+
|
|
|
141
|
+
<NavLink to="/account/addresses" style={isActiveStyle}>
|
|
142
|
+
Addresses
|
|
143
|
+
</NavLink>
|
|
144
|
+
|
|
|
145
|
+
<Logout />
|
|
146
|
+
</nav>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function Logout() {
|
|
151
|
+
return (
|
|
152
|
+
<Form className="account-logout" method="POST" action="/account/logout">
|
|
153
|
+
<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;
|
package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx
ADDED
|
@@ -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;
|