diff --git a/components/product/helpers.ts b/components/product/helpers.ts index 029476c92..381dcbc1d 100644 --- a/components/product/helpers.ts +++ b/components/product/helpers.ts @@ -14,6 +14,10 @@ export function getVariant(product: Product, opts: SelectedOptions) { option.displayName.toLowerCase() === key.toLowerCase() ) { return option.values.find((v) => v.label.toLowerCase() === value) + } else if (!value) { + return !variant.options.filter( + ({ displayName }) => displayName.toLowerCase() === key + ).length } }) ) diff --git a/framework/shopify/api/operations/get-all-collections.ts b/framework/shopify/api/operations/get-all-collections.ts deleted file mode 100644 index 9cf216a91..000000000 --- a/framework/shopify/api/operations/get-all-collections.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Client from 'shopify-buy' -import { ShopifyConfig } from '../index' - -type Options = { - config: ShopifyConfig -} - -const getAllCollections = async (options: Options) => { - const { config } = options - - const client = Client.buildClient({ - storefrontAccessToken: config.apiToken, - domain: config.commerceUrl, - }) - - const res = await client.collection.fetchAllWithProducts() - - return JSON.parse(JSON.stringify(res)) -} - -export default getAllCollections diff --git a/framework/shopify/api/operations/get-page.ts b/framework/shopify/api/operations/get-page.ts deleted file mode 100644 index 32acb7c8f..000000000 --- a/framework/shopify/api/operations/get-page.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Page } from '../../schema' -import { ShopifyConfig, getConfig } from '..' - -export type GetPageResult = T - -export type PageVariables = { - id: string -} - -async function getPage({ - url, - variables, - config, - preview, -}: { - url?: string - variables: PageVariables - config?: ShopifyConfig - preview?: boolean -}): Promise { - config = getConfig(config) - return {} -} - -export default getPage diff --git a/framework/shopify/auth/use-signup.tsx b/framework/shopify/auth/use-signup.tsx index 7f66448d3..65ed9f71d 100644 --- a/framework/shopify/auth/use-signup.tsx +++ b/framework/shopify/auth/use-signup.tsx @@ -1,15 +1,16 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' -import { CommerceError } from '@commerce/utils/errors' +import { CommerceError, ValidationError } from '@commerce/utils/errors' import useSignup, { UseSignup } from '@commerce/auth/use-signup' import useCustomer from '../customer/use-customer' -import { CustomerCreateInput } from '../schema' - import { - customerCreateMutation, - customerAccessTokenCreateMutation, -} from '../utils/mutations' -import handleLogin from '../utils/handle-login' + CustomerCreateInput, + Mutation, + MutationCustomerCreateArgs, +} from '../schema' + +import { customerCreateMutation } from '../utils/mutations' +import { handleAutomaticLogin, handleAccountActivation } from '../utils' export default useSignup as UseSignup @@ -33,7 +34,11 @@ export const handler: MutationHook< 'A first name, last name, email and password are required to signup', }) } - const data = await fetch({ + + const { customerCreate } = await fetch< + Mutation, + MutationCustomerCreateArgs + >({ ...options, variables: { input: { @@ -45,19 +50,18 @@ export const handler: MutationHook< }, }) - try { - const loginData = await fetch({ - query: customerAccessTokenCreateMutation, - variables: { - input: { - email, - password, - }, - }, + const errors = customerCreate?.customerUserErrors + + if (errors && errors.length) { + const [error] = errors + throw new ValidationError({ + message: error.message, }) - handleLogin(loginData) - } catch (error) {} - return data + } + + await handleAutomaticLogin(fetch, { email, password }) + + return null }, useHook: ({ fetch }) => () => { const { revalidate } = useCustomer() diff --git a/framework/shopify/cart/use-add-item.tsx b/framework/shopify/cart/use-add-item.tsx index d0f891148..36f02847b 100644 --- a/framework/shopify/cart/use-add-item.tsx +++ b/framework/shopify/cart/use-add-item.tsx @@ -40,8 +40,7 @@ export const handler: MutationHook = { }, }) - // TODO: Fix this Cart type here - return checkoutToCart(checkoutLineItemsAdd) as any + return checkoutToCart(checkoutLineItemsAdd) }, useHook: ({ fetch }) => () => { const { mutate } = useCart() diff --git a/framework/shopify/cart/use-cart.tsx b/framework/shopify/cart/use-cart.tsx index d154bb837..a8be04969 100644 --- a/framework/shopify/cart/use-cart.tsx +++ b/framework/shopify/cart/use-cart.tsx @@ -22,6 +22,7 @@ export const handler: SWRHook< }, async fetcher({ input: { cartId: checkoutId }, options, fetch }) { let checkout + if (checkoutId) { const data = await fetch({ ...options, @@ -36,8 +37,7 @@ export const handler: SWRHook< checkout = await checkoutCreate(fetch) } - // TODO: Fix this type - return checkoutToCart({ checkout } as any) + return checkoutToCart({ checkout }) }, useHook: ({ useData }) => (input) => { const response = useData({ diff --git a/framework/shopify/cart/utils/checkout-create.ts b/framework/shopify/cart/utils/checkout-create.ts index e950cc7e4..20c23bcd3 100644 --- a/framework/shopify/cart/utils/checkout-create.ts +++ b/framework/shopify/cart/utils/checkout-create.ts @@ -6,8 +6,11 @@ import { import checkoutCreateMutation from '../../utils/mutations/checkout-create' import Cookies from 'js-cookie' +import { CheckoutCreatePayload } from '../../schema' -export const checkoutCreate = async (fetch: any) => { +export const checkoutCreate = async ( + fetch: any +): Promise => { const data = await fetch({ query: checkoutCreateMutation, }) diff --git a/framework/shopify/cart/utils/checkout-to-cart.ts b/framework/shopify/cart/utils/checkout-to-cart.ts index 03005f342..5465e511c 100644 --- a/framework/shopify/cart/utils/checkout-to-cart.ts +++ b/framework/shopify/cart/utils/checkout-to-cart.ts @@ -5,14 +5,24 @@ import { CheckoutLineItemsAddPayload, CheckoutLineItemsRemovePayload, CheckoutLineItemsUpdatePayload, - Maybe, + CheckoutCreatePayload, + Checkout, + UserError, } from '../../schema' import { normalizeCart } from '../../utils' +import { Maybe } from 'framework/bigcommerce/schema' + +export type CheckoutQuery = { + checkout: Checkout + userErrors?: Array +} export type CheckoutPayload = | CheckoutLineItemsAddPayload | CheckoutLineItemsUpdatePayload | CheckoutLineItemsRemovePayload + | CheckoutCreatePayload + | CheckoutQuery const checkoutToCart = (checkoutPayload?: Maybe): Cart => { if (!checkoutPayload) { diff --git a/framework/shopify/product/use-search.tsx b/framework/shopify/product/use-search.tsx index 425df9e83..bf812af3d 100644 --- a/framework/shopify/product/use-search.tsx +++ b/framework/shopify/product/use-search.tsx @@ -48,7 +48,8 @@ export const handler: SWRHook< edges = data.node?.products?.edges ?? [] if (brandId) { edges = edges.filter( - ({ node: { vendor } }: ProductEdge) => vendor === brandId + ({ node: { vendor } }: ProductEdge) => + vendor.replace(/\s+/g, '-').toLowerCase() === brandId ) } } else { diff --git a/framework/shopify/utils/get-vendors.ts b/framework/shopify/utils/get-vendors.ts index f04483bb1..24843f177 100644 --- a/framework/shopify/utils/get-vendors.ts +++ b/framework/shopify/utils/get-vendors.ts @@ -2,13 +2,14 @@ import { ShopifyConfig } from '../api' import fetchAllProducts from '../api/utils/fetch-all-products' import getAllProductVendors from './queries/get-all-product-vendors-query' -export type BrandNode = { +export type Brand = { + entityId: string name: string path: string } export type BrandEdge = { - node: BrandNode + node: Brand } export type Brands = BrandEdge[] @@ -24,13 +25,16 @@ const getVendors = async (config: ShopifyConfig): Promise => { let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor) - return [...new Set(vendorsStrings)].map((v) => ({ - node: { - entityId: v, - name: v, - path: `brands/${v}`, - }, - })) + return [...new Set(vendorsStrings)].map((v) => { + const id = v.replace(/\s+/g, '-').toLowerCase() + return { + node: { + entityId: id, + name: v, + path: `brands/${id}`, + }, + } + }) } export default getVendors diff --git a/framework/shopify/utils/handle-account-activation.ts b/framework/shopify/utils/handle-account-activation.ts new file mode 100644 index 000000000..3f5aff5b8 --- /dev/null +++ b/framework/shopify/utils/handle-account-activation.ts @@ -0,0 +1,35 @@ +import { ValidationError } from '@commerce/utils/errors' +import { FetcherOptions } from '@commerce/utils/types' +import { + MutationCustomerActivateArgs, + MutationCustomerActivateByUrlArgs, +} from '../schema' +import { Mutation } from '../schema' +import { customerActivateByUrlMutation } from './mutations' + +const handleAccountActivation = async ( + fetch: (options: FetcherOptions) => Promise, + input: MutationCustomerActivateByUrlArgs +) => { + try { + const { customerActivateByUrl } = await fetch< + Mutation, + MutationCustomerActivateArgs + >({ + query: customerActivateByUrlMutation, + variables: { + input, + }, + }) + + const errors = customerActivateByUrl?.customerUserErrors + if (errors && errors.length) { + const [error] = errors + throw new ValidationError({ + message: error.message, + }) + } + } catch (error) {} +} + +export default handleAccountActivation diff --git a/framework/shopify/utils/handle-login.ts b/framework/shopify/utils/handle-login.ts index 77b6873e3..0a0a2e8ec 100644 --- a/framework/shopify/utils/handle-login.ts +++ b/framework/shopify/utils/handle-login.ts @@ -1,5 +1,8 @@ import { ValidationError } from '@commerce/utils/errors' +import { FetcherOptions } from '@commerce/utils/types' +import { CustomerAccessTokenCreateInput } from '../schema' import { setCustomerToken } from './customer-token' +import { customerAccessTokenCreateMutation } from './mutations' const getErrorMessage = ({ code, @@ -36,4 +39,19 @@ const handleLogin = (data: any) => { return customerAccessToken } +export const handleAutomaticLogin = async ( + fetch: (options: FetcherOptions) => Promise, + input: CustomerAccessTokenCreateInput +) => { + try { + const loginData = await fetch({ + query: customerAccessTokenCreateMutation, + variables: { + input, + }, + }) + handleLogin(loginData) + } catch (error) {} +} + export default handleLogin diff --git a/framework/shopify/utils/index.ts b/framework/shopify/utils/index.ts index 2d59aa506..01a5743e5 100644 --- a/framework/shopify/utils/index.ts +++ b/framework/shopify/utils/index.ts @@ -4,6 +4,8 @@ export { default as getSortVariables } from './get-sort-variables' export { default as getVendors } from './get-vendors' export { default as getCategories } from './get-categories' export { default as getCheckoutId } from './get-checkout-id' +export { default as handleLogin, handleAutomaticLogin } from './handle-login' +export { default as handleAccountActivation } from './handle-account-activation' export * from './queries' export * from './mutations' export * from './normalize' diff --git a/framework/shopify/utils/mutations/customer-activate-by-url.ts b/framework/shopify/utils/mutations/customer-activate-by-url.ts new file mode 100644 index 000000000..345d502bd --- /dev/null +++ b/framework/shopify/utils/mutations/customer-activate-by-url.ts @@ -0,0 +1,19 @@ +const customerActivateByUrlMutation = /* GraphQL */ ` + mutation customerActivateByUrl($activationUrl: URL!, $password: String!) { + customerActivateByUrl(activationUrl: $activationUrl, password: $password) { + customer { + id + } + customerAccessToken { + accessToken + expiresAt + } + customerUserErrors { + code + field + message + } + } + } +` +export default customerActivateByUrlMutation diff --git a/framework/shopify/utils/mutations/customer-activate.ts b/framework/shopify/utils/mutations/customer-activate.ts new file mode 100644 index 000000000..b1d057c69 --- /dev/null +++ b/framework/shopify/utils/mutations/customer-activate.ts @@ -0,0 +1,19 @@ +const customerActivateMutation = /* GraphQL */ ` + mutation customerActivate($id: ID!, $input: CustomerActivateInput!) { + customerActivate(id: $id, input: $input) { + customer { + id + } + customerAccessToken { + accessToken + expiresAt + } + customerUserErrors { + code + field + message + } + } + } +` +export default customerActivateMutation diff --git a/framework/shopify/utils/mutations/index.ts b/framework/shopify/utils/mutations/index.ts index 3a16d7cec..165fb192d 100644 --- a/framework/shopify/utils/mutations/index.ts +++ b/framework/shopify/utils/mutations/index.ts @@ -5,3 +5,5 @@ export { default as checkoutLineItemUpdateMutation } from './checkout-line-item- 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' +export { default as customerActivateMutation } from './customer-activate' +export { default as customerActivateByUrlMutation } from './customer-activate-by-url' diff --git a/framework/shopify/utils/normalize.ts b/framework/shopify/utils/normalize.ts index c9b428b37..5f11fe7c6 100644 --- a/framework/shopify/utils/normalize.ts +++ b/framework/shopify/utils/normalize.ts @@ -33,7 +33,7 @@ const normalizeProductOption = ({ let output: any = { label: value, } - if (displayName === 'Color') { + if (displayName.match(/colou?r/gi)) { output = { ...output, hexColors: [value], @@ -54,21 +54,24 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { return edges?.map( ({ node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 }, - }) => ({ - id, - name: title, - sku: sku ?? id, - price: +priceV2.amount, - listPrice: +compareAtPriceV2?.amount, - requiresShipping: true, - options: selectedOptions.map(({ name, value }: SelectedOption) => - normalizeProductOption({ - id, - name, - values: [value], - }) - ), - }) + }) => { + return { + id, + name: title, + sku: sku ?? id, + price: +priceV2.amount, + listPrice: +compareAtPriceV2?.amount, + requiresShipping: true, + options: selectedOptions.map(({ name, value }: SelectedOption) => { + const options = normalizeProductOption({ + id, + name, + values: [value], + }) + return options + }), + } + } ) } @@ -96,7 +99,12 @@ export function normalizeProduct(productNode: ShopifyProduct): Product { price: money(priceRange?.minVariantPrice), images: normalizeProductImages(images), variants: variants ? normalizeProductVariants(variants) : [], - options: options ? options.map((o) => normalizeProductOption(o)) : [], + options: + options + ?.filter(({ name, values }) => { + return name !== 'Title' && values !== ['Default Title'] + }) + .map((o) => normalizeProductOption(o)) ?? [], ...rest, } @@ -122,7 +130,7 @@ export function normalizeCart(checkout: Checkout): Cart { } function normalizeLineItem({ - node: { id, title, variant, quantity }, + node: { id, title, variant, quantity, ...rest }, }: CheckoutLineItemEdge): LineItem { return { id, @@ -135,7 +143,7 @@ function normalizeLineItem({ sku: variant?.sku ?? '', name: variant?.title!, image: { - url: variant?.image?.originalSrc, + url: variant?.image?.originalSrc ?? '/product-img-placeholder.svg', }, requiresShipping: variant?.requiresShipping ?? false, price: variant?.priceV2?.amount, @@ -143,10 +151,13 @@ function normalizeLineItem({ }, path: '', discounts: [], - options: [ - { - value: variant?.title, - }, - ], + options: + variant?.title !== 'Default Title' + ? [ + { + value: variant?.title, + }, + ] + : [], } } diff --git a/framework/shopify/wishlist/use-add-item.tsx b/framework/shopify/wishlist/use-add-item.tsx index 75f067c3a..438397f2b 100644 --- a/framework/shopify/wishlist/use-add-item.tsx +++ b/framework/shopify/wishlist/use-add-item.tsx @@ -1,13 +1,34 @@ import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item' -export function emptyHook() { - const useEmptyHook = async (options = {}) => { - return useCallback(async function () { - return Promise.resolve() - }, []) - } +import useCustomer from '../customer/use-customer' +import useWishlist from './use-wishlist' - return useEmptyHook +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + useHook: ({ fetch }) => () => { + const { data: customer } = useCustomer() + const { revalidate } = useWishlist() + + return useCallback( + async function addItem(item) { + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + await revalidate() + return null + }, + [fetch, revalidate, customer] + ) + }, } - -export default emptyHook diff --git a/framework/shopify/wishlist/use-remove-item.tsx b/framework/shopify/wishlist/use-remove-item.tsx index a2d3a8a05..971ec082f 100644 --- a/framework/shopify/wishlist/use-remove-item.tsx +++ b/framework/shopify/wishlist/use-remove-item.tsx @@ -1,17 +1,36 @@ import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useRemoveItem, { + UseRemoveItem, +} from '@commerce/wishlist/use-remove-item' -type Options = { - includeProducts?: boolean +import useCustomer from '../customer/use-customer' +import useWishlist from './use-wishlist' + +export default useRemoveItem as UseRemoveItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + useHook: ({ fetch }) => () => { + const { data: customer } = useCustomer() + const { revalidate } = useWishlist() + + return useCallback( + async function addItem(item) { + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + await revalidate() + return null + }, + [fetch, revalidate, customer] + ) + }, } - -export function emptyHook(options?: Options) { - const useEmptyHook = async ({ id }: { id: string | number }) => { - return useCallback(async function () { - return Promise.resolve() - }, []) - } - - return useEmptyHook -} - -export default emptyHook diff --git a/framework/shopify/wishlist/use-wishlist.tsx b/framework/shopify/wishlist/use-wishlist.tsx index d2ce9db5b..651ea06c8 100644 --- a/framework/shopify/wishlist/use-wishlist.tsx +++ b/framework/shopify/wishlist/use-wishlist.tsx @@ -1,46 +1,49 @@ -// TODO: replace this hook and other wishlist hooks with a handler, or remove them if -// Shopify doesn't have a wishlist +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' +import useCustomer from '../customer/use-customer' -import { HookFetcher } from '@commerce/utils/types' -import { Product } from '../schema' +export type UseWishlistInput = { includeProducts?: boolean } -const defaultOpts = {} +export default useWishlist as UseWishlist -export type Wishlist = { - items: [ - { - product_id: number - variant_id: number - id: number - product: Product - } - ] +export const handler: SWRHook< + any | null, + UseWishlistInput, + { customerId?: number } & UseWishlistInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + url: '/api/bigcommerce/wishlist', + method: 'GET', + }, + fetcher() { + return { items: [] } + }, + useHook: ({ useData }) => (input) => { + const { data: customer } = useCustomer() + const response = useData({ + input: [ + ['customerId', customer?.entityId], + ['includeProducts', input?.includeProducts], + ], + swrOptions: { + revalidateOnFocus: false, + ...input?.swrOptions, + }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.items?.length || 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, } - -export interface UseWishlistOptions { - includeProducts?: boolean -} - -export interface UseWishlistInput extends UseWishlistOptions { - customerId?: number -} - -export const fetcher: HookFetcher = () => { - return null -} - -export function extendHook( - customFetcher: typeof fetcher, - // swrOptions?: SwrOptions - swrOptions?: any -) { - const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => { - return { data: null } - } - - useWishlist.extend = extendHook - - return useWishlist -} - -export default extendHook(fetcher) diff --git a/pages/customer/activate.tsx b/pages/customer/activate.tsx new file mode 100644 index 000000000..61e4da0ca --- /dev/null +++ b/pages/customer/activate.tsx @@ -0,0 +1,26 @@ +import type { GetStaticPropsContext } from 'next' +import { getConfig } from '@framework/api' +import getAllPages from '@framework/common/get-all-pages' +import { Layout } from '@components/common' +import { Container, Text } from '@components/ui' + +export async function getStaticProps({ + preview, + locale, +}: GetStaticPropsContext) { + const config = getConfig({ locale }) + const { pages } = await getAllPages({ config, preview }) + return { + props: { pages }, + } +} + +export default function ActivateAccount() { + return ( + + Activate Your Account + + ) +} + +ActivateAccount.Layout = Layout diff --git a/pages/search.tsx b/pages/search.tsx index da2edccd8..4100108bc 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -75,10 +75,8 @@ export default function Search({ const { data } = useSearch({ search: typeof q === 'string' ? q : '', - // TODO: Shopify - Fix this type - categoryId: activeCategory?.entityId as any, - // TODO: Shopify - Fix this type - brandId: (activeBrand as any)?.entityId, + categoryId: activeCategory?.entityId, + brandId: activeBrand?.entityId, sort: typeof sort === 'string' ? sort : '', }) diff --git a/tsconfig.json b/tsconfig.json index e20f37099..9e712fb18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,8 +22,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/shopify"], - "@framework/*": ["framework/shopify/*"] + "@framework": ["framework/bigcommerce"], + "@framework/*": ["framework/bigcommerce/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],