mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
auth proccess
This commit is contained in:
50
app/account/component/AccountBook.tsx
Normal file
50
app/account/component/AccountBook.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import AddressCard from '@/components/AddressCard';
|
||||
import { Button } from '@/components/Button';
|
||||
import { Text } from '@/components/Text';
|
||||
import { Customer, MailingAddress } from '@/lib/shopify/types';
|
||||
import { convertObjectToQueryString } from '@/lib/utils';
|
||||
|
||||
export default function AccountBook({
|
||||
customer,
|
||||
addresses,
|
||||
}: {
|
||||
customer: Customer;
|
||||
addresses: MailingAddress[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h3 className="font-bold text-lead">Address Book</h3>
|
||||
<div>
|
||||
{!addresses?.length && (
|
||||
<Text className="mb-1" width="narrow" as="p" size="copy">
|
||||
You haven't saved any addresses yet.
|
||||
</Text>
|
||||
)}
|
||||
<div className="w-48">
|
||||
<a
|
||||
href={`account?${convertObjectToQueryString({
|
||||
modal: 'address-add',
|
||||
})}`}
|
||||
className="inline-block rounded font-medium text-center py-3 px-6 border border-primary/10 bg-contrast text-primary mt-2 text-sm w-full mb-6"
|
||||
>
|
||||
Add an Address
|
||||
</a>
|
||||
</div>
|
||||
{Boolean(addresses?.length) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{customer.defaultAddress && (
|
||||
<AddressCard address={customer.defaultAddress} defaultAddress />
|
||||
)}
|
||||
{addresses
|
||||
.filter(address => address.id !== customer.defaultAddress?.id)
|
||||
.map(address => (
|
||||
<AddressCard key={address.id} address={address} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
11
app/account/component/AuthLayout.tsx
Normal file
11
app/account/component/AuthLayout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
22
app/account/component/FormButton.tsx
Normal file
22
app/account/component/FormButton.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
import cn from 'clsx';
|
||||
export default function FormButton({
|
||||
variant = 'primary'
|
||||
}: {
|
||||
btnText: string;
|
||||
state?: string;
|
||||
variant?: 'primary' | 'outline';
|
||||
}) {
|
||||
const buttonClasses = cn({
|
||||
'bg-primary text-contrast rounded py-2 px-4 focus:shadow-outline block w-full':
|
||||
variant === 'primary',
|
||||
'text-left text-primary/50 ml-6 text-sm': variant === 'outline'
|
||||
});
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<button className={buttonClasses} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
45
app/account/component/FormFooter.tsx
Normal file
45
app/account/component/FormFooter.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
interface IFormFooter {
|
||||
page: 'login' | 'register' | 'recover';
|
||||
}
|
||||
|
||||
export default function FormFooter({ page }: IFormFooter) {
|
||||
const data = {
|
||||
login: {
|
||||
linkText: 'Create an account',
|
||||
phrase: 'New to Hydrogen?',
|
||||
href: '/account/register',
|
||||
},
|
||||
register: {
|
||||
linkText: 'Sign In',
|
||||
phrase: 'Already have an account?',
|
||||
href: '/account/login',
|
||||
},
|
||||
recover: {
|
||||
linkText: 'Login',
|
||||
phrase: 'Return to',
|
||||
href: '/account/login',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-5 mt-8 border-t border-gray-300">
|
||||
<p className="align-baseline text-sm mt-6">
|
||||
{data[page].phrase}
|
||||
|
||||
<Link className="inline underline" href={data[page].href}>
|
||||
{data[page].linkText}
|
||||
</Link>
|
||||
</p>
|
||||
{page === 'login' && (
|
||||
<Link
|
||||
className="mt-6 inline-block align-baseline text-sm text-primary/50x"
|
||||
href="/account/recover"
|
||||
>
|
||||
Forgot Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
3
app/account/component/FormHeader.tsx
Normal file
3
app/account/component/FormHeader.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function FormHeader({ title }: { title: string }) {
|
||||
return <h1 className="text-4xl">{title}</h1>;
|
||||
}
|
14
app/account/component/OrderHistory.tsx
Normal file
14
app/account/component/OrderHistory.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import EmptyOrders from '@/components/EmptyOrder';
|
||||
import Orders from '@/components/Orders';
|
||||
import { Order } from '@/lib/shopify/types';
|
||||
|
||||
export default function OrderHistory({ orders }: { orders: Order[] }) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h2 className="font-bold text-lead">Order History</h2>
|
||||
{orders?.length ? <Orders orders={orders} /> : <EmptyOrders />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
app/account/component/SignOutSection.tsx
Normal file
23
app/account/component/SignOutSection.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function SignOutSection() {
|
||||
const signOut = async () => {
|
||||
'use server';
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value: '',
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now()),
|
||||
});
|
||||
redirect('/account/login');
|
||||
};
|
||||
return (
|
||||
<form action={signOut} noValidate>
|
||||
<button type="submit" className="text-primary/50">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
114
app/account/login/page.tsx
Normal file
114
app/account/login/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { loginCustomer } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
let unidentifiedUserError: string | null = null;
|
||||
export default function LoginPage() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
const loginRes = await loginCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
?.accessToken
|
||||
) {
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value:
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
.accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerUserErrors.length > 0
|
||||
) {
|
||||
loginRes.body.data.customerAccessTokenCreate.customerUserErrors.filter(
|
||||
(error: any) => {
|
||||
if (error.field) {
|
||||
if (error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
}
|
||||
} else {
|
||||
if (error.code === 'UNIDENTIFIED_CUSTOMER') {
|
||||
unidentifiedUserError = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
revalidatePath('/account/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Sign in." />
|
||||
{unidentifiedUserError && (
|
||||
<p className="text-red-500 mt-4">{unidentifiedUserError}</p>
|
||||
)}
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText="Sign in" />
|
||||
<FormFooter page="login" />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
19
app/account/page.tsx
Normal file
19
app/account/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getCustomer } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import SignOutSection from './component/SignOutSection';
|
||||
|
||||
async function AccountPage({ searchParams }: { searchParams: { [key: string]: string } }) {
|
||||
const token = cookies().get('customerAccessToken')?.value as string;
|
||||
const customer = await getCustomer(token);
|
||||
console.log('customer', customer);
|
||||
console.log('token', token);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignOutSection />
|
||||
Account Detail
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountPage;
|
89
app/account/recover/page.tsx
Normal file
89
app/account/recover/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { recoverCustomersPassword } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let isSubmited: boolean = false;
|
||||
const headings = {
|
||||
submited: {
|
||||
title: 'Request Sent.',
|
||||
description:
|
||||
'If that email address is in our system, you will receive an email with instructions about how to reset your password in a few minutes.',
|
||||
},
|
||||
default: {
|
||||
title: 'Forgot Password.',
|
||||
description:
|
||||
'Enter the email address associated with your account to receive a link to reset your password.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RecoverPassword() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
try {
|
||||
const response = await recoverCustomersPassword({
|
||||
variables: {
|
||||
email: data.get('email') as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body.data.customerRecover.customerUserErrors.length > 0) {
|
||||
response.body.data.customerRecover.customerUserErrors.filter(
|
||||
(error: any) => {
|
||||
if (error.field && error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
isSubmited = true;
|
||||
}
|
||||
} catch (error) {
|
||||
interface ERROR {
|
||||
message: string;
|
||||
}
|
||||
const err = error as { error: ERROR };
|
||||
emailError = err.error.message;
|
||||
}
|
||||
|
||||
revalidatePath('/account/recover');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title={headings[isSubmited ? 'submited' : 'default'].title} />
|
||||
<p className="mt-4">
|
||||
{headings[isSubmited ? 'submited' : 'default'].description}
|
||||
</p>
|
||||
{!isSubmited && (
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText={'Request Reset Link'} />
|
||||
<FormFooter page="recover" />
|
||||
</form>
|
||||
)}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
114
app/account/register/page.tsx
Normal file
114
app/account/register/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { createCustomer, loginCustomer } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
|
||||
export default function RegisterPage() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
const res = await createCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (res.body.data.customerCreate.customer) {
|
||||
const loginRes = await loginCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
?.accessToken
|
||||
) {
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value:
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
.accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
redirect('/account/login');
|
||||
}
|
||||
|
||||
if (res.body.data.customerCreate.customerUserErrors.length > 0) {
|
||||
res.body.data.customerCreate.customerUserErrors.filter((error: any) => {
|
||||
if (error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
revalidatePath('/account/register');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Create an Account" />
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText="Create Account" />
|
||||
<FormFooter page="register" />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
131
app/account/reset/[id]/[resetToken]/page.tsx
Normal file
131
app/account/reset/[id]/[resetToken]/page.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { resetCustomersPassword } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../../../component/AuthLayout';
|
||||
import FormButton from '../../../component/FormButton';
|
||||
import FormHeader from '../../../component/FormHeader';
|
||||
|
||||
let errorMessage: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
let passwordConfirmError: string | null = null;
|
||||
|
||||
export default function ResetPassword({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; resetToken: string };
|
||||
}) {
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
'use server';
|
||||
const id = params.id;
|
||||
const resetToken = params.resetToken;
|
||||
|
||||
const password = data.get('password') as string;
|
||||
const passwordConfirm = data.get('passwordConfirm') as string;
|
||||
|
||||
if (
|
||||
!password ||
|
||||
!passwordConfirm ||
|
||||
typeof password !== 'string' ||
|
||||
typeof passwordConfirm !== 'string' ||
|
||||
password !== passwordConfirm
|
||||
) {
|
||||
passwordConfirmError = 'The two passwords entered did not match.';
|
||||
} else {
|
||||
const res = await resetCustomersPassword({
|
||||
variables: {
|
||||
id: `gid://shopify/Customer/${id}`,
|
||||
input: {
|
||||
password,
|
||||
resetToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customerAccessToken =
|
||||
res.body.data.customerReset.customerAccessToken;
|
||||
|
||||
if (customerAccessToken) {
|
||||
const accessToken = customerAccessToken?.accessToken;
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value: accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
if (res.body.data.customerReset.customerUserErrors.length > 0) {
|
||||
res.body.data.customerReset.customerUserErrors.filter((error: any) => {
|
||||
if (error.field) {
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
} else if (error.field.includes('passwordConfirm')) {
|
||||
passwordConfirmError = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'TOKEN_INVALID') {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
revalidatePath('/account/reset');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Reset Password." />
|
||||
<p className="mt-4">Enter a new password for your account.</p>
|
||||
{errorMessage && <p className="text-red-500 mt-4">{errorMessage}</p>}
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Re-enter password"
|
||||
aria-label="Re-enter password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordConfirmError && (
|
||||
<p className="text-red-500 text-xs">
|
||||
{' '}
|
||||
{passwordConfirmError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText={'Save'} />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user