diff --git a/app/(pages)/shopify/(components)/customer/index.js b/app/(pages)/shopify/(components)/customer/index.js new file mode 100644 index 00000000..e352951b --- /dev/null +++ b/app/(pages)/shopify/(components)/customer/index.js @@ -0,0 +1,77 @@ +'use client' + +import { Form, SubmitButton } from 'libs/form' +import { InputField } from 'libs/form/fields' + +// import { +// LoginCustomerAction, +// LogoutCustomerAction, +// CreateCustomerAction, +// } from 'libs/shopify/customer/actions' + +// function InputField({ type, id, placeholder, required, value, onChange }) { +// return ( +//
+// +//
+// ) +// } + +export function LoginForm() { + return ( +
+ + + + + ) +} + +export function RegisterForm() { + return ( +
+ + + + + + + ) +} + +export function LogoutButton() { + return ( +
+ + + ) +} diff --git a/app/(pages)/shopify/account/account.module.scss b/app/(pages)/shopify/account/account.module.scss new file mode 100644 index 00000000..e1a19c70 --- /dev/null +++ b/app/(pages)/shopify/account/account.module.scss @@ -0,0 +1,17 @@ +.page { + text-transform: uppercase; + font-family: var(--font-mono); + overflow: clip; +} + +.inner { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + flex-grow: 1; + + @include mobile { + padding: 0 mobile-vw(16px); + } +} diff --git a/app/(pages)/shopify/account/page.js b/app/(pages)/shopify/account/page.js new file mode 100644 index 00000000..58baa36f --- /dev/null +++ b/app/(pages)/shopify/account/page.js @@ -0,0 +1,48 @@ +import { getCustomer } from 'libs/shopify/customer/actions' +import { Suspense } from 'react' +import { LoginForm, LogoutButton, RegisterForm } from '../(components)/customer' +import { Wrapper } from '../../(components)/wrapper' +import s from './account.module.scss' + +export default async function AccountPage() { + const customer = await getCustomer() + + return ( + +
+

My Account

+ {customer ? ( + Loading...}> + + + + ) : ( +
+

Login

+ +

Register

+ +
+ )} +
+
+ ) +} + +function CustomerInfo({ customer }) { + return ( + <> +

Welcome, {customer.firstName}!

+

Email: {customer.email}

+

Recent Orders

+
    + {customer.orders.edges.map(({ node }) => ( +
  • + Order #{node.orderNumber} - {node.totalPrice.amount}{' '} + {node.totalPrice.currencyCode} +
  • + ))} +
+ + ) +} diff --git a/libs/form/hook.js b/libs/form/hook.js index 6a1bcc02..6300c9a7 100644 --- a/libs/form/hook.js +++ b/libs/form/hook.js @@ -27,7 +27,7 @@ export const useForm = ({ function onSubmit(event) { event.preventDefault() const formData = new FormData(event.currentTarget) - formData.append('formId', formId) + formId && formData.append('formId', formId) startTransition(async () => { await formAction(formData) diff --git a/libs/form/index.js b/libs/form/index.js index 8f43537a..a9ea5ca2 100644 --- a/libs/form/index.js +++ b/libs/form/index.js @@ -2,6 +2,11 @@ import cn from 'clsx' import { HubspotNewsletterAction } from 'libs/hubspot-forms/action' +import { + CreateCustomerAction, + LoginCustomerAction, + LogoutCustomerAction, +} from 'libs/shopify/customer/actions' import { createContext, useContext, useEffect, useState } from 'react' import s from './form.module.scss' import { useForm } from './hook' @@ -32,7 +37,7 @@ export const FormProvider = ({ const { formAction, onSubmit, ...helpers } = useForm({ action: formsActions[action], formId, - initalState: null, + initialState: null, dependencies: [], }) @@ -115,4 +120,7 @@ export const Messages = ({ className }) => { const formsActions = { HubspotNewsletterAction: HubspotNewsletterAction, + LoginCustomerAction: LoginCustomerAction, + LogoutCustomerAction: LogoutCustomerAction, + CreateCustomerAction: CreateCustomerAction, } diff --git a/libs/shopify/customer/actions.js b/libs/shopify/customer/actions.js new file mode 100644 index 00000000..0b4422c3 --- /dev/null +++ b/libs/shopify/customer/actions.js @@ -0,0 +1,124 @@ +'use server' + +import { shopifyFetch } from 'libs/shopify' +import { cookies } from 'next/headers' +import { + customerAccessTokenCreateMutation, + customerAccessTokenDeleteMutation, + customerCreateMutation, +} from '../mutations/customer' +import { getCustomerQuery } from '../queries/customer' + +export async function LoginCustomerAction(prevState, formData) { + const email = formData.get('email') + const password = formData.get('password') + + try { + const res = await shopifyFetch({ + query: customerAccessTokenCreateMutation, + variables: { + input: { + email, + password, + }, + }, + cache: 'no-store', + }) + + const { customerAccessToken, customerUserErrors } = + res.body.data.customerAccessTokenCreate + + if (customerUserErrors.length) { + return { error: customerUserErrors[0].message } + } + + if (customerAccessToken) { + cookies().set('customerAccessToken', customerAccessToken.accessToken, { + expires: new Date(customerAccessToken.expiresAt), + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + }) + } + + return { success: true } + } catch (error) { + return { error: 'An unexpected error occurred. Please try again.' } + } +} + +export async function LogoutCustomerAction() { + const customerAccessToken = cookies().get('customerAccessToken')?.value + + if (customerAccessToken) { + try { + await shopifyFetch({ + query: customerAccessTokenDeleteMutation, + variables: { + customerAccessToken, + }, + cache: 'no-store', + }) + } catch (error) { + console.error('Error during logout:', error) + } + + cookies().delete('customerAccessToken') + } + + return { success: true } +} + +export async function CreateCustomerAction(prevState, formData) { + const firstName = formData.get('firstName') + const lastName = formData.get('lastName') + const email = formData.get('email') + const password = formData.get('password') + + try { + const res = await shopifyFetch({ + query: customerCreateMutation, + variables: { + input: { + firstName, + lastName, + email, + password, + }, + }, + cache: 'no-store', + }) + + const { customer, customerUserErrors } = res.body.data.customerCreate + + if (customerUserErrors.length) { + return { error: customerUserErrors[0].message } + } + + return { success: true, customer } + } catch (error) { + return { error: 'An unexpected error occurred. Please try again.' } + } +} + +export async function getCustomer() { + const customerAccessToken = cookies().get('customerAccessToken')?.value + + if (!customerAccessToken) { + return null + } + + try { + const res = await shopifyFetch({ + query: getCustomerQuery, + variables: { + customerAccessToken, + }, + cache: 'no-store', + }) + + return res.body.data.customer + } catch (error) { + console.error('Error fetching customer data:', error) + return null + } +} diff --git a/libs/shopify/mutations/customer.js b/libs/shopify/mutations/customer.js new file mode 100644 index 00000000..490999e1 --- /dev/null +++ b/libs/shopify/mutations/customer.js @@ -0,0 +1,46 @@ +export const customerAccessTokenCreateMutation = /* GraphQL */ ` + mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) { + customerAccessTokenCreate(input: $input) { + customerAccessToken { + accessToken + expiresAt + } + customerUserErrors { + code + field + message + } + } + } +` + +export const customerAccessTokenDeleteMutation = /* GraphQL */ ` + mutation customerAccessTokenDelete($customerAccessToken: String!) { + customerAccessTokenDelete(customerAccessToken: $customerAccessToken) { + deletedAccessToken + deletedCustomerAccessTokenId + userErrors { + field + message + } + } + } +` + +export const customerCreateMutation = /* GraphQL */ ` + mutation customerCreate($input: CustomerCreateInput!) { + customerCreate(input: $input) { + customer { + id + email + firstName + lastName + } + customerUserErrors { + code + field + message + } + } + } +` diff --git a/libs/shopify/queries/customer.js b/libs/shopify/queries/customer.js new file mode 100644 index 00000000..93812277 --- /dev/null +++ b/libs/shopify/queries/customer.js @@ -0,0 +1,38 @@ +export const getCustomerQuery = /* GraphQL */ ` + query getCustomer($customerAccessToken: String!) { + customer(customerAccessToken: $customerAccessToken) { + id + firstName + lastName + email + phone + addresses(first: 5) { + edges { + node { + id + address1 + address2 + city + province + country + zip + } + } + } + orders(first: 5) { + edges { + node { + id + orderNumber + totalPrice { + amount + currencyCode + } + processedAt + fulfillmentStatus + } + } + } + } + } +`