diff --git a/framework/shopify/api/checkout/index.ts b/framework/shopify/api/checkout/index.ts index 79b38747c..638dc20ee 100644 --- a/framework/shopify/api/checkout/index.ts +++ b/framework/shopify/api/checkout/index.ts @@ -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 = async (req, res) => { +const checkoutApi: ShopifyApiHandler = 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 { diff --git a/framework/shopify/api/index.ts b/framework/shopify/api/index.ts index 7b04ed602..273c43c8b 100644 --- a/framework/shopify/api/index.ts +++ b/framework/shopify/api/index.ts @@ -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) { diff --git a/framework/shopify/auth/use-login.tsx b/framework/shopify/auth/use-login.tsx index ee447962e..516d0b14a 100644 --- a/framework/shopify/auth/use-login.tsx +++ b/framework/shopify/auth/use-login.tsx @@ -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 = ( 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 = ( 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 }) } diff --git a/framework/shopify/auth/use-logout.tsx b/framework/shopify/auth/use-logout.tsx index 75f067c3a..f8bd55579 100644 --- a/framework/shopify/auth/use-logout.tsx +++ b/framework/shopify/auth/use-logout.tsx @@ -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 = (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(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) diff --git a/framework/shopify/auth/use-signup.tsx b/framework/shopify/auth/use-signup.tsx index 75f067c3a..7184554bb 100644 --- a/framework/shopify/auth/use-signup.tsx +++ b/framework/shopify/auth/use-signup.tsx @@ -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 = ( + 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( + 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) diff --git a/framework/shopify/config.ts b/framework/shopify/config.ts index 735090185..66f2b4bc8 100644 --- a/framework/shopify/config.ts +++ b/framework/shopify/config.ts @@ -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 }), diff --git a/framework/shopify/const.ts b/framework/shopify/const.ts index 0c957632c..aa8291c02 100644 --- a/framework/shopify/const.ts +++ b/framework/shopify/const.ts @@ -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' diff --git a/framework/shopify/customer/get-customer-id.ts b/framework/shopify/customer/get-customer-id.ts new file mode 100644 index 000000000..39a9e2572 --- /dev/null +++ b/framework/shopify/customer/get-customer-id.ts @@ -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 { + config = getConfig(config) + + const { data } = await config.fetch(getCustomerIdQuery, { + variables: { + customerAccesToken: + customerAccesToken || Cookies.get(config.customerCookie), + }, + }) + + return data?.customer?.id +} + +export default getCustomerId diff --git a/framework/shopify/customer/use-customer.tsx b/framework/shopify/customer/use-customer.tsx index d46879b91..6f956d2c2 100644 --- a/framework/shopify/customer/use-customer.tsx +++ b/framework/shopify/customer/use-customer.tsx @@ -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 = async ( - options, - _, - fetch -) => { - const data = await fetch({ ...defaultOpts, ...options }) - return data?.customer ?? null +export const fetcher: HookFetcher = async (options, _, fetch) => { + const customerAccessToken = getCustomerToken() + if (customerAccessToken) { + const data = await fetch({ + ...defaultOpts, + ...options, + variables: { customerAccessToken }, + }) + return data?.customer ?? null + } + return null } export function extendHook( customFetcher: typeof fetcher, - swrOptions?: SwrOptions + swrOptions?: SwrOptions ) { const useCustomer = () => { return useCommerceCustomer(defaultOpts, [], customFetcher, { diff --git a/framework/shopify/lib/normalize.ts b/framework/shopify/lib/normalize.ts index eeb637a48..c39331cd9 100644 --- a/framework/shopify/lib/normalize.ts +++ b/framework/shopify/lib/normalize.ts @@ -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, }, diff --git a/framework/shopify/product/get-all-collections.ts b/framework/shopify/product/get-all-collections.ts index bf3fee392..b63adf159 100644 --- a/framework/shopify/product/get-all-collections.ts +++ b/framework/shopify/product/get-all-collections.ts @@ -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) => ({ diff --git a/framework/shopify/product/get-all-product-paths.ts b/framework/shopify/product/get-all-product-paths.ts index 3627321a8..e632219f7 100644 --- a/framework/shopify/product/get-all-product-paths.ts +++ b/framework/shopify/product/get-all-product-paths.ts @@ -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}`, }, diff --git a/framework/shopify/product/use-search.tsx b/framework/shopify/product/use-search.tsx index ee59c9448..51c390bba 100644 --- a/framework/shopify/product/use-search.tsx +++ b/framework/shopify/product/use-search.tsx @@ -61,7 +61,6 @@ export function extendHook( const response = useCommerceSearch( { query: getAllProductsQuery, - method: 'POST', }, [ ['search', input.search], diff --git a/framework/shopify/utils/customer-token.ts b/framework/shopify/utils/customer-token.ts new file mode 100644 index 000000000..c78a07c44 --- /dev/null +++ b/framework/shopify/utils/customer-token.ts @@ -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) + } +} diff --git a/framework/shopify/utils/mutations/associate-customer-with-checkout.ts b/framework/shopify/utils/mutations/associate-customer-with-checkout.ts new file mode 100644 index 000000000..6b1350e05 --- /dev/null +++ b/framework/shopify/utils/mutations/associate-customer-with-checkout.ts @@ -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 diff --git a/framework/shopify/utils/mutations/customer-acces-token-create.ts b/framework/shopify/utils/mutations/customer-access-token-create.ts similarity index 100% rename from framework/shopify/utils/mutations/customer-acces-token-create.ts rename to framework/shopify/utils/mutations/customer-access-token-create.ts diff --git a/framework/shopify/utils/mutations/customer-access-token-delete.ts b/framework/shopify/utils/mutations/customer-access-token-delete.ts new file mode 100644 index 000000000..c46eff1e5 --- /dev/null +++ b/framework/shopify/utils/mutations/customer-access-token-delete.ts @@ -0,0 +1,14 @@ +const customerAccessTokenDeleteMutation = /* GraphQL */ ` + mutation customerAccessTokenDelete($customerAccessToken: String!) { + customerAccessTokenDelete(customerAccessToken: $customerAccessToken) { + deletedAccessToken + deletedCustomerAccessTokenId + userErrors { + field + message + } + } + } +` + +export default customerAccessTokenDeleteMutation diff --git a/framework/shopify/utils/mutations/index.ts b/framework/shopify/utils/mutations/index.ts index 14f82a476..fe1beb82d 100644 --- a/framework/shopify/utils/mutations/index.ts +++ b/framework/shopify/utils/mutations/index.ts @@ -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' diff --git a/framework/shopify/utils/queries/get-customer-id-query.ts b/framework/shopify/utils/queries/get-customer-id-query.ts new file mode 100644 index 000000000..076ceb10b --- /dev/null +++ b/framework/shopify/utils/queries/get-customer-id-query.ts @@ -0,0 +1,8 @@ +export const getCustomerQuery = /* GraphQL */ ` + query getCustomerId($customerAccessToken: String!) { + customer(customerAccessToken: $customerAccessToken) { + id + } + } +` +export default getCustomerQuery diff --git a/framework/shopify/utils/queries/get-customer-query.ts b/framework/shopify/utils/queries/get-customer-query.ts new file mode 100644 index 000000000..87e37e68d --- /dev/null +++ b/framework/shopify/utils/queries/get-customer-query.ts @@ -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 diff --git a/framework/shopify/utils/queries/index.ts b/framework/shopify/utils/queries/index.ts index a1282f1e1..e0506cb5a 100644 --- a/framework/shopify/utils/queries/index.ts +++ b/framework/shopify/utils/queries/index.ts @@ -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'