forked from crowetic/commerce
Login, Sign Up, Log Out, and checkout & customer association
This commit is contained in:
parent
612392aaba
commit
dde09c5105
@ -3,16 +3,35 @@ import createApiHandler, {
|
||||
ShopifyApiHandler,
|
||||
} from '../utils/create-api-handler'
|
||||
|
||||
import { SHOPIFY_CHECKOUT_URL_COOKIE } from '@framework/const'
|
||||
import {
|
||||
SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||
SHOPIFY_CHECKOUT_URL_COOKIE,
|
||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||
} from '@framework/const'
|
||||
import { getConfig } from '..'
|
||||
import associateCustomerWithCheckoutMutation from '@framework/utils/mutations/associate-customer-with-checkout'
|
||||
|
||||
const METHODS = ['GET']
|
||||
|
||||
const checkoutApi: ShopifyApiHandler<any> = async (req, res) => {
|
||||
const checkoutApi: ShopifyApiHandler<any> = async (req, res, config) => {
|
||||
if (!isAllowedMethod(req, res, METHODS)) return
|
||||
|
||||
config = getConfig()
|
||||
|
||||
const { cookies } = req
|
||||
const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE]
|
||||
|
||||
try {
|
||||
await config.fetch(associateCustomerWithCheckoutMutation, {
|
||||
variables: {
|
||||
checkoutId: cookies[SHOPIFY_CHECKOUT_ID_COOKIE],
|
||||
customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (checkoutUrl) {
|
||||
res.redirect(checkoutUrl)
|
||||
} else {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import type { CommerceAPIConfig } from '@commerce/api'
|
||||
import { SHOPIFY_CHECKOUT_ID_COOKIE } from '@framework/const'
|
||||
import {
|
||||
SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||
} from '@framework/const'
|
||||
import fetchGraphqlApi from '../utils/fetch-graphql-api'
|
||||
|
||||
export interface ShopifyConfig extends CommerceAPIConfig {}
|
||||
@ -46,7 +49,7 @@ const config = new Config({
|
||||
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||
cartCookieMaxAge: ONE_DAY * 30,
|
||||
fetch: fetchGraphqlApi,
|
||||
customerCookie: 'SHOP_TOKEN',
|
||||
customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||
})
|
||||
|
||||
export function getConfig(userConfig?: Partial<ShopifyConfig>) {
|
||||
|
@ -1,21 +1,37 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useCommerceLogin from '@commerce/use-login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import createCustomerAccessTokenMutation from '../utils/mutations/customer-acces-token-create'
|
||||
import createCustomerAccessTokenMutation from '../utils/mutations/customer-access-token-create'
|
||||
import { CustomerAccessTokenCreateInput } from '@framework/schema'
|
||||
import { setCustomerToken } from '@framework/utils/customer-token'
|
||||
|
||||
const defaultOpts = {
|
||||
query: createCustomerAccessTokenMutation,
|
||||
}
|
||||
|
||||
const getErrorMessage = ({
|
||||
code,
|
||||
message,
|
||||
}: {
|
||||
code: string
|
||||
message: string
|
||||
}) => {
|
||||
switch (code) {
|
||||
case 'UNIDENTIFIED_CUSTOMER':
|
||||
message = 'Cannot find an account that matches the provided credentials'
|
||||
break
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<null, CustomerAccessTokenCreateInput> = (
|
||||
options,
|
||||
{ email, password },
|
||||
input,
|
||||
fetch
|
||||
) => {
|
||||
if (!(email && password)) {
|
||||
if (!(input.email && input.password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
@ -25,7 +41,25 @@ export const fetcher: HookFetcher<null, CustomerAccessTokenCreateInput> = (
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { email, password },
|
||||
variables: { input },
|
||||
}).then((data) => {
|
||||
const response = data?.customerAccessTokenCreate
|
||||
const errors = response?.customerUserErrors
|
||||
|
||||
if (errors && errors.length) {
|
||||
throw new ValidationError({
|
||||
message: getErrorMessage(errors[0]),
|
||||
})
|
||||
}
|
||||
|
||||
const customerAccessToken = response?.customerAccessToken
|
||||
const accessToken = customerAccessToken?.accessToken
|
||||
|
||||
if (accessToken) {
|
||||
setCustomerToken(accessToken)
|
||||
}
|
||||
|
||||
return customerAccessToken
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,45 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCommerceLogout from '@commerce/use-logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import customerAccessTokenDeleteMutation from '@framework/utils/mutations/customer-access-token-delete'
|
||||
import {
|
||||
getCustomerToken,
|
||||
setCustomerToken,
|
||||
} from '@framework/utils/customer-token'
|
||||
|
||||
export function emptyHook() {
|
||||
const useEmptyHook = async (options = {}) => {
|
||||
return useCallback(async function () {
|
||||
return Promise.resolve()
|
||||
}, [])
|
||||
}
|
||||
|
||||
return useEmptyHook
|
||||
const defaultOpts = {
|
||||
query: customerAccessTokenDeleteMutation,
|
||||
}
|
||||
|
||||
export default emptyHook
|
||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: {
|
||||
customerAccessToken: getCustomerToken(),
|
||||
},
|
||||
}).then((d) => setCustomerToken(null))
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogout = () => {
|
||||
const { mutate } = useCustomer()
|
||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login() {
|
||||
const data = await fn(null)
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogout.extend = extendHook
|
||||
|
||||
return useLogout
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,13 +1,57 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceSignup from '@commerce/use-signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import customerCreateMutation from '@framework/utils/mutations/customer-create'
|
||||
import { CustomerCreateInput } from '@framework/schema'
|
||||
|
||||
export function emptyHook() {
|
||||
const useEmptyHook = async (options = {}) => {
|
||||
return useCallback(async function () {
|
||||
return Promise.resolve()
|
||||
}, [])
|
||||
}
|
||||
|
||||
return useEmptyHook
|
||||
const defaultOpts = {
|
||||
query: customerCreateMutation,
|
||||
}
|
||||
|
||||
export default emptyHook
|
||||
export const fetcher: HookFetcher<null, CustomerCreateInput> = (
|
||||
options,
|
||||
input,
|
||||
fetch
|
||||
) => {
|
||||
if (!(input.firstName && input.lastName && input.email && input.password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: { input },
|
||||
}).then((data) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useSignup = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceSignup<null, CustomerCreateInput>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async function signup(input: CustomerCreateInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useSignup.extend = extendHook
|
||||
|
||||
return useSignup
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -32,7 +32,7 @@ const shopifyConfig: ShopifyConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||
storeDomain: STORE_DOMAIN,
|
||||
async fetcher({ method = 'POST', variables, query }) {
|
||||
async fetcher({ method = 'POST', query, variables }) {
|
||||
const res = await fetch(API_URL, {
|
||||
method,
|
||||
body: JSON.stringify({ query, variables }),
|
||||
|
@ -1,2 +1,3 @@
|
||||
export const SHOPIFY_CHECKOUT_ID_COOKIE = 'shopify_checkoutId'
|
||||
export const SHOPIFY_CHECKOUT_URL_COOKIE = 'shopify_checkoutUrl'
|
||||
export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken'
|
||||
|
24
framework/shopify/customer/get-customer-id.ts
Normal file
24
framework/shopify/customer/get-customer-id.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getConfig, ShopifyConfig } from '@framework/api'
|
||||
import getCustomerIdQuery from '@framework/utils/queries/get-customer-id-query'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
async function getCustomerId({
|
||||
customerToken: customerAccesToken,
|
||||
config,
|
||||
}: {
|
||||
customerToken: string
|
||||
config?: ShopifyConfig
|
||||
}): Promise<number | undefined> {
|
||||
config = getConfig(config)
|
||||
|
||||
const { data } = await config.fetch(getCustomerIdQuery, {
|
||||
variables: {
|
||||
customerAccesToken:
|
||||
customerAccesToken || Cookies.get(config.customerCookie),
|
||||
},
|
||||
})
|
||||
|
||||
return data?.customer?.id
|
||||
}
|
||||
|
||||
export default getCustomerId
|
@ -1,23 +1,29 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCustomer from '@commerce/use-customer'
|
||||
import getCustomerQuery from '@framework/utils/queries/get-customer-query'
|
||||
import { getCustomerToken } from '@framework/utils/customer-token'
|
||||
|
||||
const defaultOpts = {
|
||||
query: '/api/bigcommerce/customers',
|
||||
query: getCustomerQuery,
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Customer | null> = async (
|
||||
options,
|
||||
_,
|
||||
fetch
|
||||
) => {
|
||||
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
|
||||
return data?.customer ?? null
|
||||
export const fetcher: HookFetcher<any | null> = async (options, _, fetch) => {
|
||||
const customerAccessToken = getCustomerToken()
|
||||
if (customerAccessToken) {
|
||||
const data = await fetch<any | null>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: { customerAccessToken },
|
||||
})
|
||||
return data?.customer ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Customer | null>
|
||||
swrOptions?: SwrOptions<any | null>
|
||||
) {
|
||||
const useCustomer = () => {
|
||||
return useCommerceCustomer(defaultOpts, [], customFetcher, {
|
||||
|
@ -111,7 +111,7 @@ function normalizeLineItem({
|
||||
variant: {
|
||||
id: String(variant?.id),
|
||||
sku: variant?.sku ?? '',
|
||||
name: variant?.title,
|
||||
name: variant?.title!,
|
||||
image: {
|
||||
url: variant?.image?.originalSrc,
|
||||
},
|
||||
|
@ -11,7 +11,7 @@ const getAllCollections = async (options?: {
|
||||
config = getConfig(config)
|
||||
|
||||
const { data } = await config.fetch(getAllCollectionsQuery, { variables })
|
||||
const edges = data.collections?.edges ?? []
|
||||
const edges = data?.collections?.edges ?? []
|
||||
|
||||
const categories = edges.map(
|
||||
({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({
|
||||
|
@ -18,12 +18,12 @@ const getAllProductPaths = async (options?: {
|
||||
variables,
|
||||
})
|
||||
|
||||
const edges = data.products?.edges
|
||||
const productInfo = data.products?.productInfo
|
||||
const edges = data?.products?.edges
|
||||
const productInfo = data?.products?.productInfo
|
||||
const hasNextPage = productInfo?.hasNextPage
|
||||
|
||||
return {
|
||||
products: edges.map(({ node: { handle } }: ProductEdge) => ({
|
||||
products: edges?.map(({ node: { handle } }: ProductEdge) => ({
|
||||
node: {
|
||||
path: `/${handle}`,
|
||||
},
|
||||
|
@ -61,7 +61,6 @@ export function extendHook(
|
||||
const response = useCommerceSearch(
|
||||
{
|
||||
query: getAllProductsQuery,
|
||||
method: 'POST',
|
||||
},
|
||||
[
|
||||
['search', input.search],
|
||||
|
12
framework/shopify/utils/customer-token.ts
Normal file
12
framework/shopify/utils/customer-token.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import { SHOPIFY_CUSTOMER_TOKEN_COOKIE } from '@framework/const'
|
||||
|
||||
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
||||
|
||||
export const setCustomerToken = (token: string | null, options?: any) => {
|
||||
if (!token) {
|
||||
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
|
||||
} else {
|
||||
Cookies.set(SHOPIFY_CUSTOMER_TOKEN_COOKIE, token, options)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
const associateCustomerWithCheckoutMutation = /* GraphQl */ `
|
||||
mutation associateCustomerWithCheckout($checkoutId: ID!, $customerAccessToken: String!) {
|
||||
checkoutCustomerAssociateV2(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) {
|
||||
checkout {
|
||||
id
|
||||
}
|
||||
checkoutUserErrors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
customer {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export default associateCustomerWithCheckoutMutation
|
@ -0,0 +1,14 @@
|
||||
const customerAccessTokenDeleteMutation = /* GraphQL */ `
|
||||
mutation customerAccessTokenDelete($customerAccessToken: String!) {
|
||||
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
|
||||
deletedAccessToken
|
||||
deletedCustomerAccessTokenId
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default customerAccessTokenDeleteMutation
|
@ -3,3 +3,5 @@ export { default as checkoutCreateMutation } from './checkout-create'
|
||||
export { default as checkoutLineItemAddMutation } from './checkout-line-item-add'
|
||||
export { default as checkoutLineItemUpdateMutation } from './checkout-create'
|
||||
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
|
||||
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
|
||||
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
|
||||
|
8
framework/shopify/utils/queries/get-customer-id-query.ts
Normal file
8
framework/shopify/utils/queries/get-customer-id-query.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const getCustomerQuery = /* GraphQL */ `
|
||||
query getCustomerId($customerAccessToken: String!) {
|
||||
customer(customerAccessToken: $customerAccessToken) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
export default getCustomerQuery
|
16
framework/shopify/utils/queries/get-customer-query.ts
Normal file
16
framework/shopify/utils/queries/get-customer-query.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const getCustomerQuery = /* GraphQL */ `
|
||||
query getCustomer($customerAccessToken: String!) {
|
||||
customer(customerAccessToken: $customerAccessToken) {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
displayName
|
||||
email
|
||||
phone
|
||||
tags
|
||||
acceptsMarketing
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
export default getCustomerQuery
|
@ -4,3 +4,4 @@ export { default as getAllProductsQuery } from './get-all-products-query'
|
||||
export { default as getAllProductsPathtsQuery } from './get-all-products-paths-query'
|
||||
export { default as getCheckoutQuery } from './get-checkout-query'
|
||||
export { default as getAllPagesQuery } from './get-all-pages-query'
|
||||
export { default as getCustomerQuery } from './get-checkout-query'
|
||||
|
Loading…
x
Reference in New Issue
Block a user