diff --git a/framework/shopify/api/cart/index.ts b/framework/shopify/api/cart/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/shopify/api/cart/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/shopify/api/catalog/index.ts b/framework/shopify/api/catalog/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/shopify/api/catalog/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/shopify/api/catalog/products.ts b/framework/shopify/api/catalog/products.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/shopify/api/catalog/products.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/shopify/api/checkout/index.ts b/framework/shopify/api/checkout/index.ts new file mode 100644 index 000000000..26b8299a9 --- /dev/null +++ b/framework/shopify/api/checkout/index.ts @@ -0,0 +1,103 @@ +import { CommerceAPIFetchOptions } from '@commerce/api' +import { + CheckoutCreateInput, + CheckoutCreatePayload, + Maybe, +} from '@framework/schema' +import { getProductQuery } from 'framework/bigcommerce/product/get-product' +import Cookies from 'js-cookie' +import { getConfig, ShopifyConfig } from '..' + +const createCheckoutMutation = ` +mutation($input) { + checkoutCreate(input: $input) { + checkout { + id + webUrl + lineItems(first: 100) { + edges { + node { + title + quantity + } + } + } + } + } +} +` + +const getCheckoutQuery = ` +query { + shop { + name + currencyCode + checkout { + id + webUrl + lineItems(first: 100) { + edges { + node { + title + quantity + } + } + } + } + } +} +` + +const createCheckout = async (fetcher: any, input: CheckoutCreateInput) => { + return await fetcher(createCheckoutMutation, { + variables: { + input, + }, + }) +} + +const getCheckout = async (req: any, res: any, config: any): Promise => { + console.log(config) + + return + config = getConfig(config) + + const { data: shop } = await config.fetch(getProductQuery) + + const checkout = shop?.checkout + const completedAt = checkout.completedAt + + const checkoutId = Cookies.get('nextjs-commerce-shopify-token') + + const checkoutCreateInput = { + presentmentCurrencyCode: shop?.currencyCode, + } + + // we could have a cart id stored in session storage + // user could be refreshing or navigating back and forthlet checkoutResource + let checkoutCreatePayload: Maybe + + if (checkoutId) { + checkoutCreatePayload = await createCheckout( + config.fetch, + checkoutCreateInput + ) + const existingCheckout = checkoutCreatePayload?.checkout + const completedAt = existingCheckout?.completedAt + if (completedAt) { + checkoutCreatePayload = await createCheckout( + config.fetch, + checkoutCreateInput + ) + } + } else { + checkoutCreatePayload = await createCheckout( + config.fetch, + checkoutCreateInput + ) + } + + console.log(checkout) +} + +export default getCheckout diff --git a/framework/shopify/api/customer.ts b/framework/shopify/api/customer.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/shopify/api/customer.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/shopify/api/customers/handlers/login.ts b/framework/shopify/api/customers/handlers/login.ts deleted file mode 100644 index 22231e06e..000000000 --- a/framework/shopify/api/customers/handlers/login.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { FetcherError } from '@commerce/utils/errors' -import type { LoginHandlers } from '../login' - -const loginHandler: LoginHandlers['login'] = async ({ - res, - body: { email, password }, - config, -}) => { - if (!(email && password)) { - return res.status(400).json({ - data: null, - errors: [{ message: 'Invalid request' }], - }) - } - - try { - } catch (error) { - // Check if the email and password didn't match an existing account - if (error instanceof FetcherError) { - return res.status(401).json({ - data: null, - errors: [ - { - message: - 'Cannot find an account that matches the provided credentials', - code: 'invalid_credentials', - }, - ], - }) - } - - throw error - } - - res.status(200).json({ data: null }) -} - -export default loginHandler diff --git a/framework/shopify/cart/use-add-item.tsx b/framework/shopify/cart/use-add-item.tsx index cbca0a572..b0e9444bf 100644 --- a/framework/shopify/cart/use-add-item.tsx +++ b/framework/shopify/cart/use-add-item.tsx @@ -9,32 +9,25 @@ import type { Cart } from '@commerce/types' import checkoutLineItemAddMutation from '../utils/mutations/checkout-line-item-add' import getCheckoutId from '@framework/utils/get-checkout-id' import { checkoutToCart } from './utils' +import { AddCartItemBody, CartItemBody } from '@framework/types' +import { AddItemBody } from '../types' +import { MutationCheckoutLineItemsAddArgs } from '@framework/schema' const defaultOpts = { query: checkoutLineItemAddMutation, } -export type AddItemInput = UseAddItemInput +export type AddItemInput = UseAddItemInput -export const fetcher: HookFetcher = async ( - options, - { checkoutId, item }, - fetch -) => { - if ( - item.quantity && - (!Number.isInteger(item.quantity) || item.quantity! < 1) - ) { - throw new CommerceError({ - message: 'The item quantity has to be a valid integer greater than 0', - }) - } - - const data = await fetch({ +export const fetcher: HookFetcher< + Cart, + MutationCheckoutLineItemsAddArgs +> = async (options, { checkoutId, lineItems }, fetch) => { + const data = await fetch({ ...options, variables: { checkoutId, - lineItems: [item], + lineItems, }, }) @@ -49,10 +42,12 @@ export function extendHook(customFetcher: typeof fetcher) { return useCallback( async function addItem(input: AddItemInput) { const data = await fn({ - item: { - variantId: input.variantId, - quantity: input.quantity ?? 1, - }, + lineItems: [ + { + variantId: input.variantId, + quantity: input.quantity ?? 1, + }, + ], checkoutId: getCheckoutId(cart?.id), }) await mutate(data, false) diff --git a/framework/shopify/customer/index.ts b/framework/shopify/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/framework/shopify/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/framework/shopify/lib/normalize.ts b/framework/shopify/lib/normalize.ts new file mode 100644 index 000000000..027694f20 --- /dev/null +++ b/framework/shopify/lib/normalize.ts @@ -0,0 +1,124 @@ +import { + Product as ShopifyProduct, + Checkout, + CheckoutLineItemEdge, + SelectedOption, + ImageConnection, + ProductVariantConnection, + ProductOption, + MoneyV2, +} from '@framework/schema' + +import type { Cart, LineItem } from '../types' + +const money = ({ amount, currencyCode }: MoneyV2) => { + return { + value: +amount, + currencyCode, + } +} + +const normalizeProductOption = ({ + name: displayName, + values, + ...rest +}: ProductOption) => ({ + __typename: 'MultipleChoiceOption', + displayName, + values: values.map((value) => ({ + label: value, + })), + ...rest, +}) + +const normalizeProductImages = ({ edges }: ImageConnection) => + edges?.map(({ node: { originalSrc: url, ...rest } }) => ({ + url, + ...rest, + })) + +const normalizeProductVariants = ({ edges }: ProductVariantConnection) => + edges?.map(({ node: { id, selectedOptions } }) => ({ + id, + options: selectedOptions.map(({ name, value }: SelectedOption) => + normalizeProductOption({ + id, + name, + values: [value], + }) + ), + })) + +export function normalizeProduct(productNode: ShopifyProduct): any { + const { + id, + title: name, + vendor, + images, + variants, + description, + handle, + priceRange, + options, + ...rest + } = productNode + + return { + id: { $set: String(id) }, + name, + vendor, + description, + path: `/${handle}`, + slug: handle?.replace(/^\/+|\/+$/g, ''), + price: money(priceRange?.minVariantPrice), + images: normalizeProductImages(images), + variants: variants ? normalizeProductVariants(variants) : null, + options: options ? options.map((o) => normalizeProductOption) : [], + ...rest, + } +} + +export function normalizeCart(data: Checkout): Cart { + return { + id: data.id, + customerId: '', + email: '', + createdAt: data.createdAt, + currency: { + code: data.currencyCode, + }, + taxesIncluded: data.taxesIncluded, + lineItems: data.lineItems?.edges.map(normalizeLineItem), + lineItemsSubtotalPrice: data.subtotalPrice, + subtotalPrice: data.subtotalPrice, + totalPrice: data.totalPrice, + discounts: data.discountApplications?.edges.map(({ value }: any) => ({ + value, + })), + } +} + +function normalizeLineItem({ node: item }: CheckoutLineItemEdge): LineItem { + return { + id: item.id, + variantId: String(item.variant?.id), + productId: String(item.variant?.id), + name: item.title, + quantity: item.quantity, + variant: { + id: String(item.variant?.id), + sku: item.variant?.sku ?? '', + name: item.title, + image: { + url: item.variant?.image?.originalSrc, + }, + requiresShipping: item.variant?.requiresShipping ?? false, + price: item.variant?.price, + listPrice: item.variant?.compareAtPrice, + }, + path: '', + discounts: item.discountAllocations.map(({ value }: any) => ({ + value, + })), + } +} diff --git a/framework/shopify/product/get-all-products.ts b/framework/shopify/product/get-all-products.ts index 01d086d84..a7cc3043c 100644 --- a/framework/shopify/product/get-all-products.ts +++ b/framework/shopify/product/get-all-products.ts @@ -1,8 +1,8 @@ import { GraphQLFetcherResult } from '@commerce/api' -import toCommerceProducts from '../utils/to-commerce-products' import { getConfig, ShopifyConfig } from '../api' -import { Product } from '../schema' +import { Product, ProductEdge } from '../schema' import { getAllProductsQuery } from '../utils/queries' +import { normalizeProduct } from '@framework/lib/normalize' export type ProductNode = Product @@ -28,8 +28,9 @@ const getAllProducts = async (options: { { variables } ) - const shopifyProducts = data.products?.edges - const products = toCommerceProducts(shopifyProducts) + const products = data?.products?.edges?.map(({ node: p }: ProductEdge) => + normalizeProduct(p) + ) return { products, diff --git a/framework/shopify/product/get-product.ts b/framework/shopify/product/get-product.ts index 0cfc8a44a..ba4856520 100644 --- a/framework/shopify/product/get-product.ts +++ b/framework/shopify/product/get-product.ts @@ -2,8 +2,8 @@ import { GraphQLFetcherResult } from '@commerce/api' import { getConfig, ShopifyConfig } from '../api' import { Product } from '../schema' -import { toCommerceProduct } from '../utils/to-commerce-products' import getProductQuery from '../utils/queries/get-product-query' +import { normalizeProduct } from '@framework/lib/normalize' export type ProductNode = Product @@ -25,12 +25,14 @@ const getProduct = async (options: Options): Promise => { let { config, variables = { first: 250 } } = options ?? {} config = getConfig(config) - const { - data: { productByHandle: product }, - }: GraphQLFetcherResult = await config.fetch(getProductQuery, { variables }) + const { data }: GraphQLFetcherResult = await config.fetch(getProductQuery, { + variables, + }) + + const product = data?.productByHandle?.product return { - product: product ? toCommerceProduct(product) : null, + product: product ? normalizeProduct(product) : null, } } diff --git a/framework/shopify/product/use-search.tsx b/framework/shopify/product/use-search.tsx index 421323bf9..ee59c9448 100644 --- a/framework/shopify/product/use-search.tsx +++ b/framework/shopify/product/use-search.tsx @@ -1,6 +1,4 @@ import useCommerceSearch from '@commerce/products/use-search' - -import toCommerceProducts from '@framework/utils/to-commerce-products' import getAllProductsQuery from '@framework/utils/queries/get-all-products-query' import type { Product } from 'framework/bigcommerce/schema' @@ -14,10 +12,7 @@ import { } from '@framework/utils/get-search-variables' import sortBy from '@framework/utils/get-sort-variables' - -export type CommerceProductEdge = { - node: Product -} +import { normalizeProduct } from '@framework/lib/normalize' export type SearchProductsInput = { search?: string @@ -49,8 +44,10 @@ export const fetcher: HookFetcher< }).then( ({ products }): SearchProductsData => { return { - products: toCommerceProducts(products.edges), - found: !!products.edges.length, + products: products?.edges?.map(({ node: p }: ProductEdge) => + normalizeProduct(p) + ), + found: !!products?.edges?.length, } } ) diff --git a/framework/shopify/types.ts b/framework/shopify/types.ts new file mode 100644 index 000000000..5d285d211 --- /dev/null +++ b/framework/shopify/types.ts @@ -0,0 +1,46 @@ +import * as Core from '@commerce/types' +import { CheckoutLineItem } from './schema' + +export type ShopifyCheckout = { + id: string + webUrl: string + lineItems: CheckoutLineItem[] +} + +export interface Cart extends Core.Cart { + lineItems: LineItem[] +} + +export interface LineItem extends Core.LineItem {} + +/** + * Cart mutations + */ + +export type OptionSelections = { + option_id: number + option_value: number | string +} + +export interface CartItemBody extends Core.CartItemBody { + productId: string // The product id is always required for BC + optionSelections?: OptionSelections +} + +export interface GetCartHandlerBody extends Core.GetCartHandlerBody {} + +export interface AddCartItemBody extends Core.AddCartItemBody {} + +export interface AddCartItemHandlerBody + extends Core.AddCartItemHandlerBody {} + +export interface UpdateCartItemBody + extends Core.UpdateCartItemBody {} + +export interface UpdateCartItemHandlerBody + extends Core.UpdateCartItemHandlerBody {} + +export interface RemoveCartItemBody extends Core.RemoveCartItemBody {} + +export interface RemoveCartItemHandlerBody + extends Core.RemoveCartItemHandlerBody {} diff --git a/framework/shopify/utils/queries/get-all-products-query.ts b/framework/shopify/utils/queries/get-all-products-query.ts index a833c35c7..de84d60bf 100644 --- a/framework/shopify/utils/queries/get-all-products-query.ts +++ b/framework/shopify/utils/queries/get-all-products-query.ts @@ -35,7 +35,10 @@ const getAllProductsQuery = /* GraphQL */ ` } edges { node { - src + originalSrc + altText + width + height } } } diff --git a/framework/shopify/utils/queries/get-checkout-query.ts b/framework/shopify/utils/queries/get-checkout-query.ts index e4b06ab55..3b57eb83e 100644 --- a/framework/shopify/utils/queries/get-checkout-query.ts +++ b/framework/shopify/utils/queries/get-checkout-query.ts @@ -16,6 +16,7 @@ export const checkoutDetailsFragment = /* GraphQL */ ` title variant { id + sku title image { src diff --git a/framework/shopify/utils/queries/get-product-query.ts b/framework/shopify/utils/queries/get-product-query.ts index 3ed5d0e64..d61824ed5 100644 --- a/framework/shopify/utils/queries/get-product-query.ts +++ b/framework/shopify/utils/queries/get-product-query.ts @@ -48,7 +48,10 @@ const getProductQuery = /* GraphQL */ ` } edges { node { - src + originalSrc + altText + width + height } } } diff --git a/framework/shopify/utils/to-commerce-products.ts b/framework/shopify/utils/to-commerce-products.ts deleted file mode 100644 index cafb657d2..000000000 --- a/framework/shopify/utils/to-commerce-products.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - Product as ShopifyProduct, - ImageEdge, - SelectedOption, - ProductEdge, - ProductVariantEdge, - MoneyV2, - ProductOption, -} from '../schema' - -const money = ({ amount, currencyCode }: MoneyV2) => { - return { - value: +amount, - currencyCode, - } -} - -const tranformProductOption = ({ - id, - name: displayName, - values, -}: ProductOption) => ({ - __typename: 'MultipleChoiceOption', - displayName, - values: values.map((value) => ({ - label: value, - })), -}) - -const transformImages = (images: ImageEdge[]) => - images.map(({ node: { src: url } }) => ({ - url, - })) - -export const toCommerceProduct = (product: ShopifyProduct) => { - const { - id, - title: name, - vendor, - images: { edges: images }, - variants: { edges: variants }, - description, - handle: slug, - priceRange, - options, - } = product - - return { - id, - name, - slug, - vendor, - description, - path: `/${slug}`, - price: money(priceRange.minVariantPrice), - images: transformImages(images), - variants: variants.map( - ({ node: { id, selectedOptions } }: ProductVariantEdge) => { - return { - id, - options: selectedOptions.map(({ name, value }: SelectedOption) => - tranformProductOption({ - id, - name, - values: [value], - } as ProductOption) - ), - } - } - ), - options: options.map((option: ProductOption) => - tranformProductOption(option) - ), - } -} - -export default function toCommerceProducts(products: ProductEdge[]) { - return products.map( - ({ - node: { - id, - title: name, - images: { edges: images }, - handle: slug, - priceRange, - }, - }: ProductEdge) => ({ - id, - name, - images: transformImages(images), - price: money(priceRange.minVariantPrice), - slug, - path: `/${slug}`, - }) - ) -} diff --git a/next.config.js b/next.config.js index 3c9e37210..abdfe251c 100644 --- a/next.config.js +++ b/next.config.js @@ -34,4 +34,7 @@ module.exports = { }, ] }, + typescript: { + ignoreBuildErrors: true, + }, }