From 57ebfe42f56470eceebb9771bc9fdaceb2d21814 Mon Sep 17 00:00:00 2001 From: cond0r Date: Fri, 5 Feb 2021 00:02:39 +0200 Subject: [PATCH] start customer auth & signup --- .../api/utils/fetch-graphql-api.ts | 1 - framework/bigcommerce/auth/use-signup.tsx | 19 +++--- framework/shopify/api/index.ts | 6 +- framework/shopify/auth/use-login.tsx | 60 ++++++++++++++++--- framework/shopify/cart/use-cart.tsx | 1 + framework/shopify/config.ts | 44 ++++++++++++++ framework/shopify/customer/use-customer.tsx | 24 ++++---- framework/shopify/index.tsx | 55 +---------------- framework/shopify/utils/fetch-graphql-api.ts | 21 +++++-- .../mutations/customer-acces-token-create.ts | 16 +++++ .../utils/mutations/customer-create.ts | 15 +++++ framework/shopify/utils/mutations/index.ts | 1 + 12 files changed, 173 insertions(+), 90 deletions(-) create mode 100644 framework/shopify/config.ts create mode 100644 framework/shopify/utils/mutations/customer-acces-token-create.ts create mode 100644 framework/shopify/utils/mutations/customer-create.ts diff --git a/framework/bigcommerce/api/utils/fetch-graphql-api.ts b/framework/bigcommerce/api/utils/fetch-graphql-api.ts index a449b81e0..aa211dd88 100644 --- a/framework/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/framework/bigcommerce/api/utils/fetch-graphql-api.ts @@ -8,7 +8,6 @@ const fetchGraphqlApi: GraphQLFetcher = async ( { variables, preview } = {}, fetchOptions ) => { - // log.warn(query) const config = getConfig() const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { ...fetchOptions, diff --git a/framework/bigcommerce/auth/use-signup.tsx b/framework/bigcommerce/auth/use-signup.tsx index c68ce7b7a..0fb54d332 100644 --- a/framework/bigcommerce/auth/use-signup.tsx +++ b/framework/bigcommerce/auth/use-signup.tsx @@ -2,17 +2,15 @@ import { useCallback } from 'react' import type { HookFetcher } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useCommerceSignup from '@commerce/use-signup' -import type { SignupBody } from '../api/customers/signup' import useCustomer from '../customer/use-customer' +import customerCreateMutation from '@framework/utils/mutations/customer-create' +import { CustomerCreateInput } from '@framework/schema' const defaultOpts = { - url: '/api/bigcommerce/customers/signup', - method: 'POST', + query: customerCreateMutation, } -export type SignupInput = SignupBody - -export const fetcher: HookFetcher = ( +export const fetcher: HookFetcher = ( options, { firstName, lastName, email, password }, fetch @@ -27,17 +25,20 @@ export const fetcher: HookFetcher = ( return fetch({ ...defaultOpts, ...options, - body: { firstName, lastName, email, password }, + variables: { firstName, lastName, email, password }, }) } export function extendHook(customFetcher: typeof fetcher) { const useSignup = () => { const { revalidate } = useCustomer() - const fn = useCommerceSignup(defaultOpts, customFetcher) + const fn = useCommerceSignup( + defaultOpts, + customFetcher + ) return useCallback( - async function signup(input: SignupInput) { + async function signup(input: CustomerCreateInput) { const data = await fn(input) await revalidate() return data diff --git a/framework/shopify/api/index.ts b/framework/shopify/api/index.ts index f62fb3437..7b04ed602 100644 --- a/framework/shopify/api/index.ts +++ b/framework/shopify/api/index.ts @@ -1,9 +1,9 @@ import type { CommerceAPIConfig } from '@commerce/api' +import { SHOPIFY_CHECKOUT_ID_COOKIE } from '@framework/const' import fetchGraphqlApi from '../utils/fetch-graphql-api' export interface ShopifyConfig extends CommerceAPIConfig {} -// No I don't like this - will fix it later const API_URL = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN @@ -38,9 +38,13 @@ export class Config { } } +const ONE_DAY = 60 * 60 * 24 + const config = new Config({ commerceUrl: API_URL, apiToken: API_TOKEN, + cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, + cartCookieMaxAge: ONE_DAY * 30, fetch: fetchGraphqlApi, customerCookie: 'SHOP_TOKEN', }) diff --git a/framework/shopify/auth/use-login.tsx b/framework/shopify/auth/use-login.tsx index 75f067c3a..ee447962e 100644 --- a/framework/shopify/auth/use-login.tsx +++ b/framework/shopify/auth/use-login.tsx @@ -1,13 +1,55 @@ import { useCallback } from 'react' +import type { HookFetcher } from '@commerce/utils/types' +import { CommerceError } 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 { CustomerAccessTokenCreateInput } from '@framework/schema' -export function emptyHook() { - const useEmptyHook = async (options = {}) => { - return useCallback(async function () { - return Promise.resolve() - }, []) - } - - return useEmptyHook +const defaultOpts = { + query: createCustomerAccessTokenMutation, } -export default emptyHook +export const fetcher: HookFetcher = ( + options, + { email, password }, + fetch +) => { + if (!(email && password)) { + throw new CommerceError({ + message: + 'A first name, last name, email and password are required to login', + }) + } + + return fetch({ + ...defaultOpts, + ...options, + body: { email, password }, + }) +} + +export function extendHook(customFetcher: typeof fetcher) { + const useLogin = () => { + const { revalidate } = useCustomer() + const fn = useCommerceLogin( + defaultOpts, + customFetcher + ) + + return useCallback( + async function login(input: CustomerAccessTokenCreateInput) { + const data = await fn(input) + await revalidate() + return data + }, + [fn] + ) + } + + useLogin.extend = extendHook + + return useLogin +} + +export default extendHook(fetcher) diff --git a/framework/shopify/cart/use-cart.tsx b/framework/shopify/cart/use-cart.tsx index 24d4e9f94..a4b92786f 100644 --- a/framework/shopify/cart/use-cart.tsx +++ b/framework/shopify/cart/use-cart.tsx @@ -7,6 +7,7 @@ import getCheckoutQuery from '@framework/utils/queries/get-checkout-query' import { Cart } from '@commerce/types' import { checkoutToCart, checkoutCreate } from './utils' +import { getConfig } from '@framework/api' const defaultOpts = { query: getCheckoutQuery, diff --git a/framework/shopify/config.ts b/framework/shopify/config.ts new file mode 100644 index 000000000..6b1743fbf --- /dev/null +++ b/framework/shopify/config.ts @@ -0,0 +1,44 @@ +import { CommerceError, FetcherError } from '@commerce/utils/errors' +import { SHOPIFY_CHECKOUT_ID_COOKIE } from './const' +import type { CommerceConfig } from '@commerce' + +export type ShopifyConfig = { + locale: string + cartCookie: string + storeDomain: string | undefined +} & CommerceConfig + +export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN +export const API_URL = `https://${STORE_DOMAIN}/api/2021-01/graphql.json` +export const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN + +const shopifyConfig: ShopifyConfig = { + locale: 'en-us', + cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, + storeDomain: STORE_DOMAIN, + async fetcher({ method = 'POST', variables, query }) { + const res = await fetch(API_URL, { + method, + body: JSON.stringify({ query, variables }), + headers: { + 'X-Shopify-Storefront-Access-Token': API_TOKEN!, + 'Content-Type': 'application/json', + }, + }) + + const json = await res.json() + + if (json.errors) { + throw new FetcherError({ + errors: json.errors ?? [ + { message: 'Failed to fetch Shopify Storefront API' }, + ], + status: res.status, + }) + } + + return { data: json.data, res } + }, +} + +export default shopifyConfig diff --git a/framework/shopify/customer/use-customer.tsx b/framework/shopify/customer/use-customer.tsx index a909443ff..d46879b91 100644 --- a/framework/shopify/customer/use-customer.tsx +++ b/framework/shopify/customer/use-customer.tsx @@ -2,18 +2,17 @@ import type { HookFetcher } from '@commerce/utils/types' import type { SwrOptions } from '@commerce/utils/use-data' import useCommerceCustomer from '@commerce/use-customer' -const defaultOpts = {} - -export type Customer = { - entityId: number - firstName: string - lastName: string - email: string +const defaultOpts = { + query: '/api/bigcommerce/customers', } -export type CustomerData = {} -export const fetcher: HookFetcher = async () => { - return null +export const fetcher: HookFetcher = async ( + options, + _, + fetch +) => { + const data = await fetch({ ...defaultOpts, ...options }) + return data?.customer ?? null } export function extendHook( @@ -21,7 +20,10 @@ export function extendHook( swrOptions?: SwrOptions ) { const useCustomer = () => { - return { data: { firstName: null, lastName: null, email: null } } + return useCommerceCustomer(defaultOpts, [], customFetcher, { + revalidateOnFocus: false, + ...swrOptions, + }) } useCustomer.extend = extendHook diff --git a/framework/shopify/index.tsx b/framework/shopify/index.tsx index f6b8a8af6..ad54ec6ab 100644 --- a/framework/shopify/index.tsx +++ b/framework/shopify/index.tsx @@ -2,64 +2,11 @@ import { ReactNode } from 'react' import * as React from 'react' import { - CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce, } from '@commerce' -import { CommerceError, FetcherError } from '@commerce/utils/errors' -import { SHOPIFY_CHECKOUT_ID_COOKIE } from './const' - -async function getText(res: Response) { - try { - return (await res.text()) || res.statusText - } catch (error) { - return res.statusText - } -} - -async function getError(res: Response) { - if (res.headers.get('Content-Type')?.includes('application/json')) { - const data = await res.json() - - return new FetcherError({ errors: data.errors, status: res.status }) - } - return new FetcherError({ message: await getText(res), status: res.status }) -} - -export type ShopifyConfig = Partial - -export const shopifyConfig: ShopifyConfig = { - locale: 'en-us', - cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, - async fetcher({ method = 'POST', variables, query }) { - const res = await fetch( - `https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-01/graphql.json`, - { - method, - body: JSON.stringify({ query, variables }), - headers: { - 'X-Shopify-Storefront-Access-Token': - process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN, - 'Content-Type': 'application/json', - }, - } - ) - - if (res.ok) { - const { data, errors } = await res.json() - - if (errors && errors.length) { - throw new CommerceError({ - message: errors[0].message, - }) - } - return data - } - - throw await getError(res) - }, -} +import shopifyConfig, { ShopifyConfig } from './config' export type ShopifyProps = { children?: ReactNode diff --git a/framework/shopify/utils/fetch-graphql-api.ts b/framework/shopify/utils/fetch-graphql-api.ts index a690314ed..b552a7f18 100644 --- a/framework/shopify/utils/fetch-graphql-api.ts +++ b/framework/shopify/utils/fetch-graphql-api.ts @@ -1,20 +1,31 @@ import type { GraphQLFetcher } from '@commerce/api' import { FetcherError } from '@commerce/utils/errors' -import { getConfig } from '../api/index' import fetch from './fetch' +import { STORE_DOMAIN, API_URL, API_TOKEN } from '../config' + +if (!STORE_DOMAIN) { + throw new Error( + `The environment variable NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN is missing and it's required to access your store` + ) +} + +if (!API_TOKEN) { + throw new Error( + `The environment variable NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN is missing and it's required to access your store` + ) +} + const fetchGraphqlApi: GraphQLFetcher = async ( query: string, { variables } = {}, fetchOptions ) => { - const { commerceUrl, apiToken } = getConfig() - - const res = await fetch(`https://${commerceUrl}/api/2021-01/graphql.json`, { + const res = await fetch(API_URL, { ...fetchOptions, method: 'POST', headers: { - 'X-Shopify-Storefront-Access-Token': apiToken, + 'X-Shopify-Storefront-Access-Token': API_TOKEN!, ...fetchOptions?.headers, 'Content-Type': 'application/json', }, diff --git a/framework/shopify/utils/mutations/customer-acces-token-create.ts b/framework/shopify/utils/mutations/customer-acces-token-create.ts new file mode 100644 index 000000000..7a45c3f49 --- /dev/null +++ b/framework/shopify/utils/mutations/customer-acces-token-create.ts @@ -0,0 +1,16 @@ +const customerAccessTokenCreateMutation = /* GraphQL */ ` + mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) { + customerAccessTokenCreate(input: $input) { + customerAccessToken { + accessToken + expiresAt + } + customerUserErrors { + code + field + message + } + } + } +` +export default customerAccessTokenCreateMutation diff --git a/framework/shopify/utils/mutations/customer-create.ts b/framework/shopify/utils/mutations/customer-create.ts new file mode 100644 index 000000000..05c728a25 --- /dev/null +++ b/framework/shopify/utils/mutations/customer-create.ts @@ -0,0 +1,15 @@ +const customerCreateMutation = /* GraphQL */ ` + mutation customerCreate($input: CustomerCreateInput!) { + customerCreate(input: $input) { + customerUserErrors { + code + field + message + } + customer { + id + } + } + } +` +export default customerCreateMutation diff --git a/framework/shopify/utils/mutations/index.ts b/framework/shopify/utils/mutations/index.ts index 9a7a7c9c1..14f82a476 100644 --- a/framework/shopify/utils/mutations/index.ts +++ b/framework/shopify/utils/mutations/index.ts @@ -1,3 +1,4 @@ +export { default as createCustomerMutation } from './customer-create' export { default as checkoutCreateMutation } from './checkout-create' export { default as checkoutLineItemAddMutation } from './checkout-line-item-add' export { default as checkoutLineItemUpdateMutation } from './checkout-create'