Login, Sign Up, Log Out, and checkout & customer association

This commit is contained in:
cond0r 2021-02-05 10:31:04 +02:00
parent 612392aaba
commit dde09c5105
21 changed files with 276 additions and 43 deletions

View File

@ -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 {

View File

@ -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>) {

View File

@ -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
})
}

View File

@ -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)

View File

@ -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)

View File

@ -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 }),

View File

@ -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'

View 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

View File

@ -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, {

View File

@ -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,
},

View File

@ -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) => ({

View File

@ -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}`,
},

View File

@ -61,7 +61,6 @@ export function extendHook(
const response = useCommerceSearch(
{
query: getAllProductsQuery,
method: 'POST',
},
[
['search', input.search],

View 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)
}
}

View File

@ -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

View File

@ -0,0 +1,14 @@
const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
field
message
}
}
}
`
export default customerAccessTokenDeleteMutation

View File

@ -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'

View File

@ -0,0 +1,8 @@
export const getCustomerQuery = /* GraphQL */ `
query getCustomerId($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
}
}
`
export default getCustomerQuery

View 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

View File

@ -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'