diff --git a/README.md b/README.md
index 981685d2b..8f4262e7e 100644
--- a/README.md
+++ b/README.md
@@ -74,3 +74,31 @@ Your app should now be running on [localhost:3000](http://localhost:3000/).
## Vercel, Next.js Commerce, and Shopify Integration Guide
You can use this comprehensive [integration guide](http://vercel.com/docs/integrations/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel.
+
+## Shopify Customer Accounts
+
+This fork is designed to provide a basic implementation of [Shopify's new Customer Accounts API](), which will allow a customer to login into their Next.js Shopify Website to update information and view orders. It uses the concepts of Next.js middleware and server actions to implemnt the Shopify Customer Accounts API Integration. All the new code for the Customer Accounts API is included in: lib/shopify/customer folder, middleware
+
+The code for this repo is adapted for Next.js from code provided by Shopify
+
+To Set This Up, please follow:
+
+1. Get
+2. Set up URLs
+3.
+
+to do: env settings file
+
+https://shopify.dev/docs/custom-storefronts/building-with-the-customer-account-api/hydrogen
+https://shopify.dev/docs/api/customer
+
+There are several issues that make this code much more complex on NextJs:
+
+1. Get can't origin in RSC - you can get this in middleware and pass down as props
+ https://blog.stackademic.com/how-next-js-middlewares-work-103cae315163
+
+2. Can't set Cookies in RSC!
+
+So to do this correctly, we have to use a fixed origin based on ENV variables, which makes testing difficult. Can only test in one environment.
+
+And 2, we need to pass the tokens to a client component, which sets the cookies client side. We couldn't figure out any other way to get this to work.
diff --git a/app/(auth)/authorize/page.tsx b/app/(auth)/authorize/page.tsx
new file mode 100644
index 000000000..723f8befb
--- /dev/null
+++ b/app/(auth)/authorize/page.tsx
@@ -0,0 +1,25 @@
+import { headers } from 'next/headers';
+export const runtime = 'edge';
+export default async function AuthorizationPage() {
+ const headersList = headers();
+ const access = headersList.get('x-shop-access');
+ if (!access) {
+ console.log('ERROR: No access header');
+ throw new Error('No access header');
+ }
+ console.log('Authorize Access code header:', access);
+ if (access === 'denied') {
+ console.log('Access Denied for Auth');
+ throw new Error('No access allowed');
+ }
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
new file mode 100644
index 000000000..dce7c33dd
--- /dev/null
+++ b/app/(auth)/login/page.tsx
@@ -0,0 +1,16 @@
+import { LoginMessage } from 'components/auth/login-message';
+export const runtime = 'edge'; //this needs to be here on thie page. I don't know why
+
+export default async function LoginPage() {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/app/(auth)/logout/page.tsx b/app/(auth)/logout/page.tsx
new file mode 100644
index 000000000..3b7080c25
--- /dev/null
+++ b/app/(auth)/logout/page.tsx
@@ -0,0 +1,13 @@
+export const runtime = 'edge';
+
+export default async function LogoutPage() {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/components/account/account-orders-history.tsx b/components/account/account-orders-history.tsx
new file mode 100644
index 000000000..6ad8046c3
--- /dev/null
+++ b/components/account/account-orders-history.tsx
@@ -0,0 +1,41 @@
+'use client';
+type OrderCardsProps = {
+ orders: any;
+};
+
+export function AccountOrdersHistory({ orders }: { orders: any }) {
+ return (
+
+
+
Order History
+ {orders?.length ? : }
+
+
+ );
+}
+
+function EmptyOrders() {
+ return (
+
+
You haven't placed any orders yet.
+
+
+ Start Shopping
+
+
+
+ );
+}
+
+function Orders({ orders }: OrderCardsProps) {
+ return (
+
+ {orders.map((order: any) => (
+ {order.node.number}
+ ))}
+
+ );
+}
diff --git a/components/account/account-profile.tsx b/components/account/account-profile.tsx
new file mode 100644
index 000000000..b560fe3a7
--- /dev/null
+++ b/components/account/account-profile.tsx
@@ -0,0 +1,53 @@
+'use client';
+import clsx from 'clsx';
+import { LogOutIcon, TriangleIcon } from '@heroicons/react/24/outline';
+import { doLogout } from './actions';
+import LoadingDots from 'components/loading-dots';
+import { useFormState, useFormStatus } from 'react-dom';
+import { Alert, AlertDescription, AlertTitle } from 'components/ui/alert';
+
+function SubmitButton(props: any) {
+ const { pending } = useFormStatus();
+ const buttonClasses =
+ 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
+ return (
+ <>
+ ) => {
+ if (pending) e.preventDefault();
+ }}
+ aria-label="Log Out"
+ aria-disabled={pending}
+ className={clsx(buttonClasses, {
+ 'hover:opacity-90': true,
+ 'cursor-not-allowed opacity-60 hover:opacity-60': pending
+ })}
+ >
+
+ {pending ? : }
+
+ {pending ? 'Logging out...' : 'Log Out'}
+
+ {props?.message && (
+
+
+ Error
+ {props?.message}
+
+ )}
+ >
+ );
+}
+
+export function AccountProfile() {
+ const [message, formAction] = useFormState(doLogout, null);
+
+ return (
+
+ );
+}
diff --git a/components/account/actions.ts b/components/account/actions.ts
new file mode 100644
index 000000000..1c4d52679
--- /dev/null
+++ b/components/account/actions.ts
@@ -0,0 +1,40 @@
+'use server';
+
+import { TAGS } from 'lib/shopify/customer/constants';
+import { removeAllCookiesServerAction } from 'lib/shopify/customer/auth-helpers';
+import { redirect } from 'next/navigation';
+import { revalidateTag } from 'next/cache';
+import { cookies } from 'next/headers';
+import { SHOPIFY_ORIGIN, SHOPIFY_CUSTOMER_ACCOUNT_API_URL } from 'lib/shopify/customer/constants';
+//import {generateCodeVerifier,generateCodeChallenge,generateRandomString} from 'lib/shopify/customer/auth-utils'
+
+export async function doLogout(prevState: any) {
+ //let logoutUrl = '/logout'
+ const origin = SHOPIFY_ORIGIN;
+ const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
+ let logoutUrl;
+ try {
+ const idToken = cookies().get('shop_id_token');
+ const idTokenValue = idToken?.value;
+ if (!idTokenValue) {
+ //you can also throw an error here with page and middleware
+ //throw new Error ("Error No Id Token")
+ //if there is no idToken, then sending to logout url will redirect shopify, so just
+ //redirect to login here and delete cookies (presumably they don't even exist)
+ logoutUrl = new URL(`${origin}/login`);
+ } else {
+ logoutUrl = new URL(
+ `${customerAccountApiUrl}/auth/logout?id_token_hint=${idTokenValue}&post_logout_redirect_uri=${origin}`
+ );
+ }
+ await removeAllCookiesServerAction();
+ revalidateTag(TAGS.customer);
+ } catch (e) {
+ console.log('Error', e);
+ //you can throw error here or return - return goes back to form b/c of state, throw will throw the error boundary
+ //throw new Error ("Error")
+ return 'Error logging out. Please try again';
+ }
+
+ redirect(`${logoutUrl}`); // Navigate to the new post page
+}
diff --git a/components/auth/actions.ts b/components/auth/actions.ts
new file mode 100644
index 000000000..36517cfd9
--- /dev/null
+++ b/components/auth/actions.ts
@@ -0,0 +1,69 @@
+//See https://react.dev/reference/react-dom/hooks/useFormState
+//https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms
+'use server';
+
+import { TAGS } from 'lib/shopify/customer/constants';
+import { redirect } from 'next/navigation';
+import { revalidateTag } from 'next/cache';
+import { cookies } from 'next/headers';
+//import { getOrigin } from 'lib/shopify/customer'
+import {
+ generateCodeVerifier,
+ generateCodeChallenge,
+ generateRandomString
+} from 'lib/shopify/customer/auth-utils';
+import {
+ SHOPIFY_CUSTOMER_ACCOUNT_API_URL,
+ SHOPIFY_CLIENT_ID,
+ SHOPIFY_ORIGIN
+} from 'lib/shopify/customer/constants';
+
+export async function doLogin(prevState: any) {
+ const customerAccountApiUrl = SHOPIFY_CUSTOMER_ACCOUNT_API_URL;
+ const clientId = SHOPIFY_CLIENT_ID;
+ const origin = SHOPIFY_ORIGIN;
+ const loginUrl = new URL(`${customerAccountApiUrl}/auth/oauth/authorize`);
+ //console.log ("previous", prevState)
+
+ try {
+ //await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
+ loginUrl.searchParams.set('client_id', clientId);
+ loginUrl.searchParams.append('response_type', 'code');
+ loginUrl.searchParams.append('redirect_uri', `${origin}/authorize`);
+ loginUrl.searchParams.set(
+ 'scope',
+ 'openid email https://api.customers.com/auth/customer.graphql'
+ );
+ const verifier = await generateCodeVerifier();
+ //const newVerifier = verifier.replace("+", '_').replace("-",'_').replace("/",'_').trim()
+ const challenge = await generateCodeChallenge(verifier);
+ cookies().set('shop_verifier', verifier as string, {
+ // @ts-ignore
+ //expires: auth?.expires, //not necessary here
+ });
+ const state = await generateRandomString();
+ const nonce = await generateRandomString();
+ cookies().set('shop_state', state as string, {
+ // @ts-ignore
+ //expires: auth?.expires, //not necessary here
+ });
+ cookies().set('shop_nonce', nonce as string, {
+ // @ts-ignore
+ //expires: auth?.expires, //not necessary here
+ });
+ loginUrl.searchParams.append('state', state);
+ loginUrl.searchParams.append('nonce', nonce);
+ loginUrl.searchParams.append('code_challenge', challenge);
+ loginUrl.searchParams.append('code_challenge_method', 'S256');
+ //console.log ("loginURL", loginUrl)
+ //throw new Error ("Error") //this is how you throw an error, if you want to. Then the catch will execute
+ } catch (e) {
+ console.log('Error', e);
+ //you can throw error here or return - return goes back to form b/c of state, throw will throw the error boundary
+ //throw new Error ("Error")
+ return 'Error logging in. Please try again';
+ }
+
+ revalidateTag(TAGS.customer);
+ redirect(`${loginUrl}`); // Navigate to the new post page
+}
diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx
new file mode 100644
index 000000000..3730c5296
--- /dev/null
+++ b/components/auth/login-form.tsx
@@ -0,0 +1,62 @@
+'use client';
+import { ExclamationTriangleIcon, UserIcon as LogInIcon } from '@heroicons/react/24/outline';
+import clsx from 'clsx';
+import { doLogin } from './actions';
+import LoadingDots from 'components/loading-dots';
+import { useFormState, useFormStatus } from 'react-dom';
+import { Alert, AlertTitle } from 'components/ui/alert';
+import { Button } from 'components/ui/button';
+
+function SubmitButton(props: any) {
+ const { pending } = useFormStatus();
+ const buttonClasses =
+ 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
+ //const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
+
+ return (
+ <>
+ {props?.message && (
+
+
+ {props?.message}
+
+ )}
+ ) => {
+ if (pending) e.preventDefault();
+ }}
+ aria-label="Log in"
+ aria-disabled={pending}
+ className={clsx(buttonClasses, {
+ 'hover:opacity-90': true,
+ 'cursor-not-allowed opacity-60 hover:opacity-60': pending
+ })}
+ >
+ {pending ? (
+ <>
+
+ Logging In
+ >
+ ) : (
+ <>
+
+ Log-In
+ >
+ )}
+
+ >
+ );
+}
+
+export function LoginShopify() {
+ const [message, formAction] = useFormState(doLogin, null);
+
+ return (
+
+ );
+}
diff --git a/components/auth/login-message.tsx b/components/auth/login-message.tsx
new file mode 100644
index 000000000..c2ee33461
--- /dev/null
+++ b/components/auth/login-message.tsx
@@ -0,0 +1,12 @@
+import { TriangleIcon } from 'lucide-react';
+import { Alert, AlertDescription, AlertTitle } from 'components/ui/alert';
+
+export function LoginMessage() {
+ return (
+
+
+ Error
+ Your session has expired. Please log in again.
+
+ );
+}
diff --git a/components/auth/login.tsx b/components/auth/login.tsx
new file mode 100644
index 000000000..8346013e1
--- /dev/null
+++ b/components/auth/login.tsx
@@ -0,0 +1,19 @@
+import { cookies } from 'next/headers';
+import { LoginShopify } from 'components/auth/login-form';
+import { UserIcon } from 'components/auth/user-icon';
+
+export default async function Login() {
+ const customerToken = cookies().get('shop_customer_token')?.value;
+ const refreshToken = cookies().get('shop_refresh_token')?.value;
+ let isLoggedIn;
+ //obviously just checking for the cookies without verifying the cookie itself is not ideal. However, the cookie is validated on the
+ //account page, so a "fake" cookie does nothing, except show the UI and then it would be deleted when clicking on account
+ //so for now, just checking the cookie for the UI is sufficient. Alternatively, we can do a query here, or a custom JWT
+ if (!customerToken && !refreshToken) {
+ isLoggedIn = false;
+ } else {
+ isLoggedIn = true;
+ }
+ console.log('LoggedIn', isLoggedIn);
+ return isLoggedIn ? : ;
+}
diff --git a/components/auth/user-icon.tsx b/components/auth/user-icon.tsx
new file mode 100644
index 000000000..40e3ded66
--- /dev/null
+++ b/components/auth/user-icon.tsx
@@ -0,0 +1,33 @@
+'use client';
+import { User2Icon } from 'lucide-react';
+import clsx from 'clsx';
+import { Button } from 'components/ui/button';
+
+function UserButton(props: any) {
+ const buttonClasses =
+ 'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
+ //const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
+
+ return (
+ <>
+
+ {/*Purposesly a href here and NOT Link component b/c of router caching*/}
+
+
+ Profile
+
+
+ >
+ );
+}
+
+export function UserIcon() {
+ return ;
+}
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 000000000..c8b3e4960
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,50 @@
+import type { NextRequest } from 'next/server';
+import { isLoggedIn, getOrigin, authorizeFn, logoutFn } from 'lib/shopify/customer';
+
+// This function can be marked `async` if using `await` inside
+export async function middleware(request: NextRequest) {
+ /****
+ Authorize Middleware to get access tokens
+ *****/
+ if (request.nextUrl.pathname.startsWith('/authorize')) {
+ console.log('Running Initial Authorization Middleware');
+ const origin = getOrigin(request);
+ //console.log ("origin", origin)
+ return await authorizeFn(request, origin);
+ }
+ /****
+ END OF Authorize Middleware to get access tokens
+ *****/
+
+ /****
+ LOGOUT -
+ *****/
+ if (request.nextUrl.pathname.startsWith('/logout')) {
+ console.log('Running Logout middleware');
+ const origin = getOrigin(request);
+ return await logoutFn(request, origin);
+ }
+ /****
+ END OF LOGOUT
+ *****/
+ /****
+ Account
+ *****/
+
+ if (request.nextUrl.pathname.startsWith('/account')) {
+ console.log('Running Account middleware');
+ //const newHeaders = new Headers(request.headers)
+ const origin = getOrigin(request);
+ //console.log ("origin", origin)
+ //just cleaner to return everything in this one function and not have to pass back shit back and forth with booleans
+ return await isLoggedIn(request, origin);
+ }
+
+ /****
+ END OF Account
+ *****/
+}
+
+export const config = {
+ matcher: ['/authorize', '/logout', '/account']
+};