diff --git a/lib/bigcommerce/api/customers/handlers/logout.ts b/lib/bigcommerce/api/customers/handlers/logout.ts new file mode 100644 index 000000000..937ce0954 --- /dev/null +++ b/lib/bigcommerce/api/customers/handlers/logout.ts @@ -0,0 +1,23 @@ +import { serialize } from 'cookie' +import { LogoutHandlers } from '../logout' + +const logoutHandler: LogoutHandlers['logout'] = async ({ + res, + body: { redirectTo }, + config, +}) => { + // Remove the cookie + res.setHeader( + 'Set-Cookie', + serialize(config.customerCookie, '', { maxAge: -1, path: '/' }) + ) + + // Only allow redirects to a relative URL + if (redirectTo?.startsWith('/')) { + res.redirect(redirectTo) + } else { + res.status(200).json({ data: null }) + } +} + +export default logoutHandler diff --git a/lib/bigcommerce/api/customers/logout.ts b/lib/bigcommerce/api/customers/logout.ts new file mode 100644 index 000000000..0a4965245 --- /dev/null +++ b/lib/bigcommerce/api/customers/logout.ts @@ -0,0 +1,42 @@ +import createApiHandler, { + BigcommerceApiHandler, + BigcommerceHandler, +} from '../utils/create-api-handler' +import isAllowedMethod from '../utils/is-allowed-method' +import { BigcommerceApiError } from '../utils/errors' +import logout from './handlers/logout' + +export type LogoutHandlers = { + logout: BigcommerceHandler +} + +const METHODS = ['GET'] + +const logoutApi: BigcommerceApiHandler = async ( + req, + res, + config, + handlers +) => { + if (!isAllowedMethod(req, res, METHODS)) return + + try { + const redirectTo = req.query.redirect_to + const body = typeof redirectTo === 'string' ? { redirectTo } : {} + + return await handlers['logout']({ req, res, config, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof BigcommerceApiError + ? 'An unexpected error ocurred with the Bigcommerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +const handlers = { logout } + +export default createApiHandler(logoutApi, handlers, {}) diff --git a/lib/bigcommerce/api/index.ts b/lib/bigcommerce/api/index.ts index 69e26f523..1ab33756c 100644 --- a/lib/bigcommerce/api/index.ts +++ b/lib/bigcommerce/api/index.ts @@ -66,9 +66,12 @@ if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) { export class Config { private config: BigcommerceConfig - constructor(config: BigcommerceConfigOptions) { + constructor(config: Omit) { this.config = { ...config, + // The customerCookie is not customizable for now, BC sets the cookie and it's + // not important to rename it + customerCookie: 'SHOP_TOKEN', imageVariables: this.getImageVariables(config.images), } } diff --git a/lib/bigcommerce/api/operations/get-all-product-paths.ts b/lib/bigcommerce/api/operations/get-all-product-paths.ts index 483160f6b..9aea42126 100644 --- a/lib/bigcommerce/api/operations/get-all-product-paths.ts +++ b/lib/bigcommerce/api/operations/get-all-product-paths.ts @@ -1,12 +1,15 @@ -import type { GetAllProductPathsQuery } from 'lib/bigcommerce/schema' +import type { + GetAllProductPathsQuery, + GetAllProductPathsQueryVariables, +} from 'lib/bigcommerce/schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import filterEdges from '../utils/filter-edges' import { BigcommerceConfig, getConfig } from '..' export const getAllProductPathsQuery = /* GraphQL */ ` - query getAllProductPaths { + query getAllProductPaths($first: Int = 100) { site { - products { + products(first: $first) { edges { node { path @@ -23,11 +26,14 @@ export type ProductPath = NonNullable< export type ProductPaths = ProductPath[] +export type { GetAllProductPathsQueryVariables } + export type GetAllProductPathsResult< T extends { products: any[] } = { products: ProductPaths } > = T async function getAllProductPaths(opts?: { + variables?: GetAllProductPathsQueryVariables config?: BigcommerceConfig }): Promise @@ -36,14 +42,17 @@ async function getAllProductPaths< V = any >(opts: { query: string + variables?: V config?: BigcommerceConfig }): Promise> async function getAllProductPaths({ query = getAllProductPathsQuery, + variables, config, }: { query?: string + variables?: GetAllProductPathsQueryVariables config?: BigcommerceConfig } = {}): Promise { config = getConfig(config) @@ -51,7 +60,7 @@ async function getAllProductPaths({ // required in case there's a custom `query` const { data } = await config.fetch< RecursivePartial - >(query) + >(query, { variables }) const products = data.site?.products?.edges return { diff --git a/lib/bigcommerce/api/utils/create-api-handler.ts b/lib/bigcommerce/api/utils/create-api-handler.ts index c6363cb15..315ec464b 100644 --- a/lib/bigcommerce/api/utils/create-api-handler.ts +++ b/lib/bigcommerce/api/utils/create-api-handler.ts @@ -14,7 +14,7 @@ export type BigcommerceApiHandler< options: Options ) => void | Promise -export type BigcommerceHandler = (options: { +export type BigcommerceHandler = (options: { req: NextApiRequest res: NextApiResponse> config: BigcommerceConfig diff --git a/lib/bigcommerce/schema.d.ts b/lib/bigcommerce/schema.d.ts index 361d45a30..cf7168992 100644 --- a/lib/bigcommerce/schema.d.ts +++ b/lib/bigcommerce/schema.d.ts @@ -1827,7 +1827,9 @@ export type ProductConnnectionFragment = { > } -export type GetAllProductPathsQueryVariables = Exact<{ [key: string]: never }> +export type GetAllProductPathsQueryVariables = Exact<{ + first?: Maybe +}> export type GetAllProductPathsQuery = { __typename?: 'Query' } & { site: { __typename?: 'Site' } & { diff --git a/lib/bigcommerce/use-logout.tsx b/lib/bigcommerce/use-logout.tsx new file mode 100644 index 000000000..f4b5f9beb --- /dev/null +++ b/lib/bigcommerce/use-logout.tsx @@ -0,0 +1,35 @@ +import { useCallback } from 'react' +import type { HookFetcher } from '@lib/commerce/utils/types' +import useCommerceLogout from '@lib/commerce/use-logout' + +const defaultOpts = { + url: '/api/bigcommerce/customers/logout', + method: 'GET', +} + +export const fetcher: HookFetcher = (options, _, fetch) => { + return fetch({ + ...defaultOpts, + ...options, + }) +} + +export function extendHook(customFetcher: typeof fetcher) { + const useLogout = () => { + const fn = useCommerceLogout(defaultOpts, customFetcher) + + return useCallback( + async function login() { + const data = await fn(null) + return data + }, + [fn] + ) + } + + useLogout.extend = extendHook + + return useLogout +} + +export default extendHook(fetcher) diff --git a/lib/commerce/api/index.ts b/lib/commerce/api/index.ts index db55e1daa..ae1e3f46b 100644 --- a/lib/commerce/api/index.ts +++ b/lib/commerce/api/index.ts @@ -3,6 +3,7 @@ export interface CommerceAPIConfig { apiToken: string cartCookie: string cartCookieMaxAge: number + customerCookie: string fetch( query: string, queryData?: CommerceAPIFetchOptions, diff --git a/lib/commerce/index.tsx b/lib/commerce/index.tsx index 3627e3774..8fc156922 100644 --- a/lib/commerce/index.tsx +++ b/lib/commerce/index.tsx @@ -21,7 +21,7 @@ export type CommerceConfig = { fetcher: Fetcher } & Omit< > export type CommerceContextValue = { - fetcherRef: MutableRefObject + fetcherRef: MutableRefObject> locale: string cartCookie: string } diff --git a/lib/commerce/use-logout.tsx b/lib/commerce/use-logout.tsx new file mode 100644 index 000000000..ef3fc4135 --- /dev/null +++ b/lib/commerce/use-logout.tsx @@ -0,0 +1,5 @@ +import useAction from './utils/use-action' + +const useLogout = useAction + +export default useLogout diff --git a/lib/commerce/utils/types.ts b/lib/commerce/utils/types.ts index 0af754fc0..81f05da3e 100644 --- a/lib/commerce/utils/types.ts +++ b/lib/commerce/utils/types.ts @@ -9,7 +9,7 @@ export type FetcherOptions = { body?: any } -export type HookFetcher = ( +export type HookFetcher = ( options: HookFetcherOptions | null, input: Input, fetch: Fetcher @@ -22,5 +22,3 @@ export type HookFetcherOptions = { } export type HookInput = [string, string | number | undefined][] - -export type HookDeps = string | number | undefined[] diff --git a/lib/commerce/utils/use-action.tsx b/lib/commerce/utils/use-action.tsx index e60cae06b..24593383f 100644 --- a/lib/commerce/utils/use-action.tsx +++ b/lib/commerce/utils/use-action.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react' import type { HookFetcher, HookFetcherOptions } from './types' import { useCommerce } from '..' -export default function useAction( +export default function useAction( options: HookFetcherOptions, fetcher: HookFetcher ) { diff --git a/next.config.js b/next.config.js index b487800af..7e00abe9b 100644 --- a/next.config.js +++ b/next.config.js @@ -18,6 +18,12 @@ module.exports = { source: '/checkout', destination: '/api/bigcommerce/checkout', }, + // The logout is also an action so this route is not required, but it's also another way + // you can allow a logout! + { + source: '/logout', + destination: '/api/bigcommerce/customers/logout?redirect_to=/', + }, ] }, } diff --git a/pages/api/bigcommerce/customers/logout.ts b/pages/api/bigcommerce/customers/logout.ts new file mode 100644 index 000000000..84ac2c7bf --- /dev/null +++ b/pages/api/bigcommerce/customers/logout.ts @@ -0,0 +1,3 @@ +import logoutApi from '@lib/bigcommerce/api/customers/logout' + +export default logoutApi() diff --git a/pages/login.tsx b/pages/login.tsx index 4fc5c328a..ae92455fd 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -2,10 +2,12 @@ import useSignup from '@lib/bigcommerce/use-signup' import { Layout } from '@components/core' import { Logo, Modal, Button } from '@components/ui' import useLogin from '@lib/bigcommerce/use-login' +import useLogout from '@lib/bigcommerce/use-logout' export default function Login() { const signup = useSignup() const login = useLogin() + const logout = useLogout() // TODO: use this method. It can take more than 5 seconds to do a signup const handleSignup = async () => { // TODO: validate the password and email before calling the signup diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 94acc2445..7d73b859c 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -27,6 +27,7 @@ export async function getStaticPaths() { return { paths: products.map((product) => `/product${product.node.path}`), + // If your store has tons of products, enable fallback mode to improve build times! fallback: false, } }