-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Shopify Customer Account API #1305
Open
osseonews
wants to merge
8
commits into
vercel:main
Choose a base branch
from
osseonews:customer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d4fd03d
Initial Customer Files
osseonews 3ed3e34
Additional Files
osseonews b972e23
Additional Files and Edits
osseonews f2fcbde
checkout from cart to keep user logged in
osseonews 41469d3
fixing checkout url
osseonews bd832f0
final changes
osseonews 06f83f6
Fix to readme
osseonews 8c4e8f9
Delete replit.nix
osseonews File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,10 @@ yarn-error.log* | |
|
||
# vercel | ||
.vercel | ||
.local | ||
.upm | ||
.replit | ||
.replit.nix | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<div className="mx-auto max-w-screen-2xl px-4"> | ||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8"> | ||
<div className="h-full w-full">Loading...</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<div className="mx-auto max-w-screen-2xl px-4"> | ||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8"> | ||
<div className="h-full w-full"> | ||
<LoginMessage /> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const runtime = 'edge'; | ||
|
||
export default async function LogoutPage() { | ||
return ( | ||
<> | ||
<div className="mx-auto max-w-screen-2xl px-4"> | ||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8"> | ||
<div className="h-full w-full">Loading...</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { headers } from 'next/headers'; | ||
import { AccountProfile } from 'components/account/account-profile'; | ||
import { AccountOrdersHistory } from 'components/account/account-orders-history'; | ||
import { redirect } from 'next/navigation'; | ||
import { shopifyCustomerFetch } from 'lib/shopify/customer/index'; | ||
import { CUSTOMER_DETAILS_QUERY } from 'lib/shopify/customer/queries/customer'; | ||
import { CustomerDetailsData } from 'lib/shopify/customer/types'; | ||
import { TAGS } from 'lib/shopify/customer/constants'; | ||
export const runtime = 'edge'; | ||
export default async function AccountPage() { | ||
const headersList = headers(); | ||
const access = headersList.get('x-shop-customer-token'); | ||
if (!access) { | ||
console.log('ERROR: No access header account'); | ||
//I'm not sure what's better here. Throw error or just log out?? | ||
//redirect gets rid of call cookies | ||
redirect('/logout'); | ||
//throw new Error("No access header") | ||
} | ||
//console.log("Authorize Access code header:", access) | ||
if (access === 'denied') { | ||
console.log('Access Denied for Auth account'); | ||
redirect('/logout'); | ||
//throw new Error("No access allowed") | ||
} | ||
const customerAccessToken = access; | ||
|
||
//this is needed b/c of strange way server components handle redirects etc. | ||
//see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting | ||
//can only redirect outside of try/catch! | ||
let success = true; | ||
let errorMessage; | ||
let customerData; | ||
let orders; | ||
|
||
try { | ||
const responseCustomerDetails = await shopifyCustomerFetch<CustomerDetailsData>({ | ||
customerToken: customerAccessToken, | ||
cache: 'no-store', | ||
query: CUSTOMER_DETAILS_QUERY, | ||
tags: [TAGS.customer] | ||
}); | ||
//console.log("userDetails", responseCustomerDetails) | ||
const userDetails = responseCustomerDetails.body; | ||
if (!userDetails) { | ||
throw new Error('Error getting actual user data Account page.'); | ||
} | ||
customerData = userDetails?.data?.customer; | ||
orders = customerData?.orders?.edges; | ||
//console.log ("Details",orders) | ||
} catch (e) { | ||
//they don't recognize this error in TS! | ||
//@ts-ignore | ||
errorMessage = e?.error?.toString() ?? 'Unknown Error'; | ||
console.log('error customer fetch account', e); | ||
if (errorMessage !== 'unauthorized') { | ||
throw new Error('Error getting actual user data Account page.'); | ||
} else { | ||
console.log('Unauthorized access. Set to false and redirect'); | ||
success = false; | ||
} | ||
} | ||
if (!success && errorMessage === 'unauthorized') redirect('/logout'); | ||
//revalidateTag('posts') // Update cached posts //FIX | ||
|
||
return ( | ||
<> | ||
<div className="mx-auto max-w-screen-2xl px-4"> | ||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row lg:gap-8"> | ||
<div className="h-full w-full"> | ||
<div> Welcome: {customerData?.emailAddress.emailAddress}</div> | ||
</div> | ||
<div className="h-full w-full"> | ||
<div className="mt-5"> | ||
<AccountProfile /> | ||
</div> | ||
</div> | ||
<div className="h-full w-full"> | ||
<div className="mt-5">{orders && <AccountOrdersHistory orders={orders} />}</div> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
'use client'; | ||
type OrderCardsProps = { | ||
orders: any; | ||
}; | ||
|
||
export function AccountOrdersHistory({ orders }: { orders: any }) { | ||
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="text-lead font-bold">Order History</h2> | ||
{orders?.length ? <Orders orders={orders} /> : <EmptyOrders />} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
function EmptyOrders() { | ||
return ( | ||
<div> | ||
<div className="mb-1">You haven't placed any orders yet.</div> | ||
<div className="w-48"> | ||
<button | ||
className="mt-2 w-full text-sm" | ||
//variant="secondary" | ||
> | ||
Start Shopping | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
function Orders({ orders }: OrderCardsProps) { | ||
return ( | ||
<ul className="false grid grid-flow-row grid-cols-1 gap-2 gap-y-6 sm:grid-cols-3 md:gap-4 lg:gap-6"> | ||
{orders.map((order: any) => ( | ||
<li key={order.node.id}>{order.node.number}</li> | ||
))} | ||
</ul> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
'use client'; | ||
import clsx from 'clsx'; | ||
import { ArrowRightIcon as LogOutIcon } from '@heroicons/react/24/outline'; | ||
import { doLogout } from './actions'; | ||
import LoadingDots from 'components/loading-dots'; | ||
import { useFormState, useFormStatus } from 'react-dom'; | ||
|
||
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 ( | ||
<> | ||
<button | ||
onClick={(e: React.FormEvent<HTMLButtonElement>) => { | ||
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 | ||
})} | ||
> | ||
<div className="absolute left-0 ml-4"> | ||
{pending ? <LoadingDots className="mb-3 bg-white" /> : <LogOutIcon className="h-5" />} | ||
</div> | ||
{pending ? 'Logging out...' : 'Log Out'} | ||
</button> | ||
{props?.message && <div className="my-5">{props?.message}</div>} | ||
</> | ||
); | ||
} | ||
|
||
export function AccountProfile() { | ||
const [message, formAction] = useFormState(doLogout, null); | ||
|
||
return ( | ||
<form action={formAction}> | ||
<SubmitButton message={message} /> | ||
<p aria-live="polite" className="sr-only" role="status"> | ||
{message} | ||
</p> | ||
</form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headers()
returns a promise, so we should useawait
here:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamesyanyuk Thanks. Yeah, this is a change in Next 15. In Next 14, which this was originally built on, header was synchronous. There might be other changes related to Next 15 that need to be fixed. I haven't checked.