UNPKG

@stackend/api

Version:

JS bindings to api.stackend.com

1,346 lines (1,169 loc) 30.2 kB
import { COMMUNITY_PARAMETER, getJson, isRunningServerSide, post, Thunk, XcapJsonResult, XcapOptionalParameters } from '../api'; import { getLocale } from '../util'; import { forEachGraphQLList, GraphQLList, PaginatedGraphQLList, PaginatedGraphQLRequest } from '../util/graphql'; import { ShopConfig, ShopDefaults } from './shopReducer'; import { Community } from '../stackend'; import { CommunityState } from '../stackend/communityReducer'; import * as ShopifyClientside from './shopify-clientside'; export interface SlimProductImage { altText: string | null; /** * The url to the scaled version of the image */ url: string; } export interface ProductImage extends SlimProductImage { /** * The url to the original version of the image */ url__originalSrc: string; } export interface MoneyV2 { amount: string; currencyCode: string; } export type WeightUnit = 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'; /** * Options for the product: size, color etc */ export interface ProductOption { id: string; name: string; values: Array<string>; } export interface SelectedProductOption { name: string; value: string; } export type SelectedProductOptions = Array<SelectedProductOption>; export interface MetaField { key: string; value: string; } /** * A variant of a product */ export interface ProductVariant { id: string; title: string; availableForSale: boolean; sku: string; image: ProductImage | null; price: MoneyV2; /** * Original price, when selling at a reduced price. May be null */ compareAtPrice: MoneyV2 | null; selectedOptions: SelectedProductOptions; weight: number; weightUnit: WeightUnit; } /** * Highest and lowest variant prices */ export interface PriceRange { minVariantPrice: MoneyV2; maxVariantPrice: MoneyV2; } export interface SlimProduct { id: string; /** permalink */ handle: string; title: string; /** Format: "2019-07-11T14:09:26Z" */ updatedAt: string; /** Format: "2019-07-11T14:09:26Z" */ createdAt: string; availableForSale: boolean; /** * Product type */ productType: string; /** Images. Actual number of images and size depends on context/listing */ images: GraphQLList<SlimProductImage>; /** Max and min price */ priceRange: PriceRange; /** List of collection handles */ collections: GraphQLList<{ handle: string }>; /** * Custom add to cart URL used by some integrators */ metafield__stackendAddToCartLink: MetaField | null; } /** * A product, including variants */ export interface Product extends SlimProduct { /** * Description as html */ descriptionHtml: string; /** Vendor name */ vendor: string; /** * Tags */ tags: Array<string>; /** * Product options */ options: Array<ProductOption>; /** * Variants of the product */ variants: GraphQLList<ProductVariant>; /** Images. Actual number of images and size depends on context/listing */ images: GraphQLList<ProductImage>; } export interface Country { name: string; code: string; continent: string; phoneNumberPrefix: number; autocompletionField: string; provinceKey: | 'COUNTY' | 'EMIRATE' | 'GOVERNORATE' | 'PREFECTURE' | 'PROVINCE' | 'REGION' | 'STATE_AND_TERRITORY' | 'STATE'; labels: { address1: string; address2: string; city: string; company: string; country: string; firstName: string; lastName: string; phone: string; postalCode: string; zone: string; }; optionalLabels: { address2: string; }; formatting: { edit: string; show: string; }; } export enum AddressFieldName { FirstName = 'firstName', LastName = 'lastName', Country = 'country', City = 'city', PostalCode = 'zip', Zone = 'province', Address1 = 'address1', Address2 = 'address2', Phone = 'phone', Company = 'company' } export interface MultipleProductListingsResult { [key: string]: { request: ListProductsQuery; listing: PaginatedGraphQLList<SlimProduct>; }; } /** * Result when requesting multiple shop related items using init/data */ export interface ShopDataResult { products: { [handle: string]: Product }; collections: { [handle: string]: Collection }; listings: MultipleProductListingsResult; shopifyDomainReferenceUrlId: number; } export interface GetShopConfigurationResult extends XcapJsonResult { shop: string | null; storeFrontAccessToken: string | null; webhookKey: string | null; enableCartNotifications: boolean; } /** * Check if the shop is enabled * @param community */ export function isShopEnabled(community: Community): boolean { if (!community) { return false; } return typeof community.settings.shop != 'undefined'; } /** * Get shopify configuration from store * @return null if disabled */ export function getShopifyConfig(): Thunk<ShopConfig | null> { return (dispatch: any, getState): ShopConfig | null => { const communities: CommunityState = getState().communities; const community = communities.community; if (!community || !community.settings.shop) { return null; } return { domain: community.settings.shop.domain, accessToken: community.settings.shop.at, countryCode: community.settings.shop.countryCode, apiVersion: ShopifyClientside.API_VERSION }; }; } /** * Get the shop configuration. Requires admin privs * @returns {Thunk<XcapJsonResult>} */ export function getShopConfiguration(): Thunk<Promise<GetShopConfigurationResult>> { return getJson({ url: '/shop/admin/get-config' }); } export interface StoreShopConfigurationResult extends XcapJsonResult { stackendCommunity: Community; } /** * Store the shop configuration. Requires admin privs * @param shop * @param storeFrontAccessToken * @param webhookKey * @param enableCartNotifications * @returns {Thunk<XcapJsonResult>} */ export function storeShopConfiguration({ shop, storeFrontAccessToken, webhookKey, enableCartNotifications = false }: { shop: string | null; storeFrontAccessToken: string | null; webhookKey: string | null; enableCartNotifications: boolean; } & XcapOptionalParameters): Thunk<Promise<StoreShopConfigurationResult>> { return post({ url: '/shop/admin/store-config', parameters: arguments }); } export interface ListProductTypesRequest extends XcapOptionalParameters { first?: number; } export interface ListProductTypesResult extends XcapJsonResult { productTypes: GraphQLList<string>; } /** * List product types * @param req * @returns {Thunk<ListProductTypesResult>} */ export function listProductTypes(req: ListProductTypesRequest): Thunk<Promise<ListProductTypesResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/list-product-types', parameters: arguments }); } else { return ShopifyClientside.listProductTypes(req); } } export enum ProductSortKeys { VENDOR = 'VENDOR', CREATED_AT = 'CREATED_AT', ID = 'ID', PRICE = 'PRICE', PRODUCT_TYPE = 'PRODUCT_TYPE', RELEVANCE = 'RELEVANCE', TITLE = 'TITLE', UPDATED_AT = 'UPDATED_AT', BEST_SELLING = 'BEST_SELLING' } /** * Parse a product sort key * @param sort * @param defaultValue */ export function parseProductSortKey(sort: string | null | undefined, defaultValue?: ProductSortKeys): ProductSortKeys { if (!sort) { return defaultValue || ProductSortKeys.RELEVANCE; } const v = (ProductSortKeys as any)[sort]; return v || defaultValue || ProductSortKeys.RELEVANCE; } export interface ListProductsQuery extends PaginatedGraphQLRequest { q?: string; productTypes?: Array<string>; tags?: Array<string>; sort?: ProductSortKeys; imageMaxWidth?: number; } export interface ListProductsRequest extends ListProductsQuery, XcapOptionalParameters {} export interface ListProductsResult extends XcapJsonResult { products: PaginatedGraphQLList<SlimProduct>; } export function applyDefaults(req: ListProductsQuery, defaults: ShopDefaults): void { if (!req.imageMaxWidth) { req.imageMaxWidth = defaults.listingImageMaxWidth; } if (req.after) { if (!req.first) { req.first = defaults.pageSize; } } else if (req.before) { if (!req.last) { req.last = defaults.pageSize; } } else { if (!req.first && !req.after) { req.first = defaults.pageSize; } } } /** * Create a new ListProductsRequest with default options * @param req */ export const newListProductsRequest = (req?: Partial<ListProductsRequest>): Thunk<ListProductsRequest> => (dispatch: any, getState: any): ListProductsRequest => { if (!req) { req = {}; } applyDefaults(req, getState().shop.defaults); return req; }; /** * List products * @param req * @returns {Thunk<ListProductsResult>} */ export function listProducts(req: ListProductsRequest): Thunk<Promise<ListProductsResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/list-products', parameters: arguments }); } else { return ShopifyClientside.listProducts(req); } } export interface GetProductRequest extends XcapOptionalParameters { handle: string; imageMaxWidth?: number; } export interface GetProductResult extends XcapJsonResult { product: Product | null; } /** * Create a new GetProductRequest with default image sizes * @param req GetProductRequest or handle */ export const newGetProductRequest = (req: GetProductRequest | string): Thunk<GetProductRequest> => (dispatch: any, getState: any): GetProductRequest => { if (typeof req === 'string') { req = { handle: req }; } const defaults: ShopDefaults = getState().shop.defaults; if (!req.imageMaxWidth) { req.imageMaxWidth = defaults.imageMaxWidth; } return req; }; /** * Get a single product * @param req * @returns {Thunk<XcapJsonResult>} */ export function getProduct(req: GetProductRequest): Thunk<Promise<GetProductResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/get-product', parameters: arguments }); } else { return ShopifyClientside.getProduct(req); } } export interface GetProductsRequest extends XcapOptionalParameters { handles: Array<string>; imageMaxWidth?: number; } export interface GetProductsResult extends XcapJsonResult { products: { [handle: string]: Product; }; } /** * Create a new GetProductsRequest with default image sizes * @param req GetProductsRequest or handles */ export const newGetProductsRequest = (req: GetProductsRequest | Array<string>): Thunk<GetProductsRequest> => (dispatch: any, getState: any): GetProductsRequest => { if (Array.isArray(req)) { req = { handles: req }; } if (!req.imageMaxWidth) { const defaults: ShopDefaults = getState().shop.defaults; req.imageMaxWidth = defaults.imageMaxWidth; } return req; }; /** * Get multiple products * @param req * @returns {Thunk<XcapJsonResult>} */ export function getProducts(req: GetProductsRequest): Thunk<Promise<GetProductsResult>> { return getJson({ url: '/shop/get-products', parameters: arguments }); // FIXME: Add client side version } export interface ListProductsAndTypesResult extends ListProductsResult { productTypes: GraphQLList<string>; } /** * List products and types * @param req * @returns {Thunk<XcapJsonResult>} */ export function listProductsAndTypes(req: ListProductsRequest): Thunk<Promise<ListProductsAndTypesResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/list-products-and-types', parameters: arguments }); } else { return ShopifyClientside.listProductsAndTypes(req); } } /** * Get the first image of a product * @param product */ export function getFirstImage(product: SlimProduct | Product | null): ProductImage | SlimProductImage | null { if (!product) { return null; } const images = product.images; if (!images || images.edges.length === 0) { return null; } return images.edges[0].node; } /** * Get a product variant * @param product * @param variant */ export function getProductVariant(product: Product, variant: string): ProductVariant | null { const n = product.variants.edges.find(v => { return v.node.id === variant; }); return n ? n.node : null; } /** * Iterate product variants * @param product * @param predicate */ export function forEachProductVariant( product: Product, predicate: (variant: ProductVariant, index: number, product: Product) => void ): void { product.variants.edges.forEach((x, index) => { predicate(x.node, index, product); }); } /** * Map each product variant * @param product * @param apply */ export function mapProductVariants<T>( product: Product, apply: (variant: ProductVariant, product: Product) => T ): Array<T> { return product.variants.edges.map(x => apply(x.node, product)); } /** * Get the variant image */ export function getVariantImage(product: Product, variant: string): ProductImage | null { const v = getProductVariant(product, variant); return v ? v.image : null; } /** * Get the lowest variant price available * @param product */ export function getLowestVariantPrice(product: Product): MoneyV2 | null { let p: MoneyV2 | null = null; forEachProductVariant(product, variant => { if (p === null || variant.price.amount < p.amount) { p = variant.price; } }); return p; } /** * Find a product variant given an image * @param product * @param image */ export function findProductVariantByImage(product: Product, image: ProductImage): ProductVariant | null { if (!product || !image) { return null; } const x = product.variants.edges.find(v => { return v.node?.image?.url === image.url; }); return x ? x.node : null; } export interface ProductSelection { [name: string]: string; } /** * Given a selection, find the product variant * @param product * @param selection */ export function findExactProductVariant(product: Product, selection: ProductSelection): ProductVariant | null { if (!product || !selection || product.variants.edges.length === 0) { return null; } const x = product.variants.edges.find(v => matchSelection(v.node, selection, false)); return x ? x.node : null; } export function findAllProductVariants(product: Product, selection: ProductSelection): Array<ProductVariant> { if (!product || !selection || product.variants.edges.length === 0) { return []; } const m: Array<ProductVariant> = []; product.variants.edges.forEach(v => { if (matchSelection(v.node, selection, true)) { m.push(v.node); } }); return m; } /** * Check if a product variant matches the selection * @param variant * @param selection * @param returnPartialMatches */ export function matchSelection( variant: ProductVariant, selection: ProductSelection, returnPartialMatches: boolean ): boolean { if (!variant) { return false; } for (const o of variant.selectedOptions) { const x = selection[o.name]; if (x) { if (x !== o.value) { return false; } } else { if (!returnPartialMatches) { return false; } } } return true; } /** * Given a variant, construct the corresponding selection * @param product * @param variant * @returns {Selection} */ export function getProductSelection(product: Product | null, variant: ProductVariant | null): ProductSelection { const s: ProductSelection = {}; if (!product || !variant) { return s; } for (const o of variant.selectedOptions) { s[o.name] = o.value; } return s; } /** * Get unique product images, including variant images * @param product */ export function getAllUniqueImages(product: Product): Array<ProductImage> { const images: Array<ProductImage> = []; const s: Set<string> = new Set<string>(); if (!product) { return images; } forEachGraphQLList(product.images, img => { if (!s.has(img.url)) { images.push(img); s.add(img.url); } }); forEachGraphQLList(product.variants, v => { const img = v.image; if (img && !s.has(img.url)) { images.push(img); s.add(img.url); } }); return images; } /** * A named collection. Slim version used in listings */ export interface SlimCollection { id: string; description: string; title: string; handle: string; } /** * A named collection of products */ export interface Collection extends SlimCollection { descriptionHtml: string; products: GraphQLList<SlimProduct>; } export interface GetCollectionRequest extends XcapOptionalParameters { handle: string; imageMaxWidth?: number; } export interface GetCollectionResult extends XcapJsonResult { collection: Collection | null; } /** * Get a collection of products * @param req * @returns {Thunk<XcapJsonResult>} */ export function getCollection(req: GetCollectionRequest): Thunk<Promise<GetCollectionResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/get-collection', parameters: arguments }); } else { return ShopifyClientside.getCollection(req); } } export interface GetCollectionsResult extends XcapJsonResult { collections: GraphQLList<SlimCollection>; } export type GetCollectionsRequest = XcapOptionalParameters; /** * Get all collections. Requires community admin privileges. * @param req * @returns {Thunk<XcapJsonResult>} */ export function getCollections(req: GetCollectionsRequest): Thunk<Promise<GetCollectionsResult>> { if (isRunningServerSide()) { return getJson({ url: '/shop/get-collections', parameters: arguments }); } else { return ShopifyClientside.getCollections(req); } } export interface CreateCartLine { /** Cart line id */ id?: string; /** * Product variant id "gid://shopify/ProductVariant/1" */ merchandiseId: string; /** * Quantity */ quantity?: number; } export interface CreateCartRequest { lines: Array<CreateCartLine>; buyerIdentity?: CartBuyerIdentity; imageMaxWidth?: number; } export interface CartLine { id: string; merchandise: { /** * Product variant id */ id: string; product: { /** Product id */ id: string; /** Product handle */ handle: string; }; }; quantity: number; discountAllocations: Array<any>; attributes: Array<{ key: string; value: string }>; estimatedCost: { subtotalAmount: MoneyV2; totalAmount: MoneyV2; }; } export interface CartRequest { cartId: string; imageMaxWidth?: number; } export interface Cart { id: string; createdAt: string; updatedAt: string; lines: GraphQLList<CartLine>; estimatedCost: { totalAmount: MoneyV2; subtotalAmount: MoneyV2; totalTaxAmount: MoneyV2 | null; totalDutyAmount: MoneyV2 | null; }; attributes: Array<any>; buyerIdentity: { countryCode: string; }; } export interface GetCartResult extends XcapJsonResult { cart: Cart | null; } export interface ModifyCartResult extends GetCartResult { userErrors?: Array<UserError>; } /** * Create a cart * @param req */ export function createCart(req: CreateCartRequest): Thunk<Promise<ModifyCartResult>> { return ShopifyClientside.createCart(req); } /** * Alter the contents of the cart. You can not add new lines using this request * @param req */ export function cartLinesUpdate(req: CartLinesUpdateRequest): Thunk<Promise<ModifyCartResult>> { return ShopifyClientside.cartLinesUpdate(req); } /** * Add products of the cart * @param req */ export function cartLinesAdd(req: CartLinesUpdateRequest): Thunk<Promise<ModifyCartResult>> { return ShopifyClientside.cartLinesAdd(req); } export type GetCartRequest = CartRequest; /** * Get a cart * @param req */ export function getCart(req: GetCartRequest): Thunk<Promise<GetCartResult>> { return ShopifyClientside.getCart(req); } export interface CartLinesUpdateRequest extends CartRequest, CreateCartRequest {} export interface CartLinesRemoveRequest extends CartRequest { lineIds: Array<string>; } /** * Remove a product line from the cart * @param req */ export function cartLinesRemove(req: CartLinesRemoveRequest): Thunk<Promise<ModifyCartResult>> { return ShopifyClientside.cartLinesRemove(req); } export interface CartBuyerIdentity { countryCode?: string; customerAccessToken?: string; email?: string; phone?: string; } export interface CartBuyerIdentityUpdateRequest extends CartRequest { buyerIdentity: CartBuyerIdentity; } /** * Update buyer identity or country * @param req */ export function cartBuyerIdentityUpdate(req: CartBuyerIdentityUpdateRequest): Thunk<Promise<ModifyCartResult>> { return ShopifyClientside.cartBuyerIdentityUpdate(req); } export interface LineItem { quantity: number; variantId: string; } export type LineItemArray = Array<LineItem>; export interface ShippingAddress { firstName: string; lastName: string; address1: string; address2?: string; zip: string; city: string; province?: string; country: string; countryCodeV2: string; company?: string; phone?: string; } export interface CreateCheckoutInput { email?: string; note?: string; lineItems?: LineItemArray; shippingAddress?: ShippingAddress; } export interface CreateCheckoutRequest extends XcapOptionalParameters { input: CreateCheckoutInput; addedProductHandle?: string; variantId?: string; quantity?: number; referenceUrlId?: string; } export interface UserError { field: string; code: string; message: string; } export interface ShippingRate { handle: string; title: string; price: MoneyV2; } export interface CheckoutLineItem { id: string; title: string; quantity: number; variant: { id: string; product: { id: string; handle: string; }; }; } export interface Checkout { id: string; ready: boolean; webUrl: string; requiresShipping: boolean; currencyCode: string; email: string | null; note: string | null; /** Price of the checkout before duties, shipping and taxes. */ subtotalPrice: MoneyV2; /** The sum of all the prices of all the items in the checkout. Duties, taxes, shipping and discounts excluded */ lineItemsSubtotalPrice: MoneyV2; /** The sum of all the prices of all the items in the checkout, duties, taxes and discounts included. */ totalPrice: MoneyV2; /** The amount left to be paid. This is equal to the cost of the line items, duties, taxes and shipping minus discounts and gift cards.*/ paymentDue: MoneyV2; /** The sum of all the taxes applied to the line items and shipping lines in the checkout. */ totalTax: MoneyV2; /** Specifies if taxes are included in the line item and shipping line prices. */ taxesIncluded: boolean; availableShippingRates?: { ready: boolean; shippingRates: Array<ShippingRate>; }; shippingAddress: ShippingAddress | null; shippingLine: ShippingRate | null; /** The date and time when the checkout was completed. */ completedAt: string | null; /** The Order Status Page for this Checkout, null when checkout is not completed. */ orderStatusUrl: string | null; /** Items in basket */ lineItems: GraphQLList<CheckoutLineItem>; } export interface CheckoutResult extends XcapJsonResult { response: { checkoutUserErrors: Array<UserError>; checkout: Checkout | null; }; } /** * Create a checkout * @param req */ export function createCheckout(req: CreateCheckoutRequest): Thunk<Promise<CheckoutResult>> { return post({ url: '/shop/checkout/create', parameters: { ...req, input: JSON.stringify(req.input) } }); } export interface GetCheckoutRequest extends XcapOptionalParameters { checkoutId: string; imageMaxWidth?: number; } export interface GetCheckoutResult extends XcapJsonResult { checkout: Checkout | null; } /** * Get a checkout given an id * @param req */ export function getCheckout(req: GetCheckoutRequest): Thunk<Promise<GetCheckoutResult>> { return getJson({ url: '/shop/checkout/get', parameters: arguments }); } export interface CheckoutReplaceItemsRequest extends XcapOptionalParameters { checkoutId: string; lineItems: LineItemArray; addedProductHandle?: string; variantId?: string; quantity?: number; referenceUrlId?: string; } /** * Replace the items in the checkout * @param req */ export function checkoutReplaceItems(req: CheckoutReplaceItemsRequest): Thunk<Promise<CheckoutResult>> { return post({ url: '/shop/checkout/replace-items', parameters: { ...req, lineItems: JSON.stringify(req.lineItems) } }); } export interface SelectShippingRequest extends XcapOptionalParameters { checkoutId: string; shippingRateHandle: string; } /** * Select shipping * @param req */ export function selectShipping(req: SelectShippingRequest): Thunk<Promise<CheckoutResult>> { return post({ url: '/shop/checkout/set-shipping', parameters: arguments }); } export interface SetCheckoutEmailRequest extends XcapOptionalParameters { checkoutId: string; email: string; } /** * Set email * @param req */ export function setCheckoutEmail(req: SetCheckoutEmailRequest): Thunk<Promise<CheckoutResult>> { return post({ url: '/shop/checkout/set-email', parameters: arguments }); } export interface SetShippingAddressRequest extends XcapOptionalParameters { checkoutId: string; address: ShippingAddress; } /** * Set shipping address * @param req */ export function setShippingAddress(req: SetShippingAddressRequest): Thunk<Promise<CheckoutResult>> { const p = { checkoutId: req.checkoutId, addressJson: JSON.stringify(req.address), [COMMUNITY_PARAMETER]: req[COMMUNITY_PARAMETER] }; return post({ url: '/shop/checkout/set-shipping-address', parameters: p }); } /** * Get the required address fields for the country using the specified locale. * @param locale (Optional. falls back to community locale) * @param countryCode */ export function getAddressFields({ locale, countryCode }: { locale?: string | null; countryCode: string; }): Thunk<Promise<AddressFieldName[][]>> { return async (dispatch: any): Promise<AddressFieldName[][]> => { const l = await dispatch(getLocale(locale)); const r = await dispatch( getJson({ url: '/shop/countries/get-address-fields', parameters: { locale: l, countryCode } }) ); return r.error ? [] : r.addressFields; }; } /** * Get the list of countries * @param locale */ export function getCountries({ locale }: { locale?: string }): Thunk<Promise<Array<Country>>> { return async (dispatch: any): Promise<Array<Country>> => { const l = await dispatch(getLocale(locale)); const r = await dispatch( getJson({ url: '/shop/countries/get-all', parameters: { locale: l } }) ); return r.error ? [] : r.countries; }; } /** * Get a country * @param locale * @param countryCode */ export function getCountry({ locale, countryCode }: { locale?: string; countryCode: string; }): Thunk<Promise<Country | null>> { return async (dispatch: any): Promise<Country | null> => { const l = await dispatch(getLocale(locale)); const r = await dispatch( getJson({ url: '/shop/countries/get', parameters: { locale: l, countryCode } }) ); return r.error ? null : r.country; }; } const CURRENCY_FORMATS: { [key: string]: Intl.NumberFormat } = {}; /** * Convert the amount to MoneyV2, adjusting to the currency correct number of decimals * @param amount * @param currencyCode */ export function toMoneyV2(amount: number, currencyCode: string): MoneyV2 { let fmt = CURRENCY_FORMATS[currencyCode]; if (!fmt) { fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: currencyCode, useGrouping: false }); CURRENCY_FORMATS[currencyCode] = fmt; } let a = fmt.format(amount); // Remove currency code a = a.replace(/[^0-9.\\-]/g, ''); return { amount: a, currencyCode: currencyCode }; } /** * Get the root nodes from a flat array of product types * @param productTypes */ export function getProductTypeRoots(productTypes: Array<string> | null | undefined): Array<string> { if (!productTypes) { return []; } const x: Set<string> = new Set(); productTypes.forEach(p => { if (p === '') { return; } const v: string = p; const i = v.indexOf('/'); if (i === -1) { x.add(v); } else { x.add(v.substring(0, i)); } }); return Array.from(x); } /** * Get the parent product type * @param productType */ export function getParentProductType(productType: string | null | undefined): string | null { if (!productType) { return null; } const i = productType.lastIndexOf('/'); if (i === -1) { return null; } return productType.substring(0, i); } export function cartFindLine(cart: Cart, productVariantId: string): CartLine | null { for (const e of cart.lines.edges) { if (e.node.merchandise.id == productVariantId) { return e.node; } } return null; } /** * Turn a cart into checkout line items * @param cart */ export function cartToLineItems(cart: Cart): Array<LineItem> { const lineItems: Array<LineItem> = []; if (cart) { forEachGraphQLList(cart.lines, i => { const l: LineItem = { variantId: i.merchandise.id, quantity: i.quantity }; lineItems.push(l); }); } return lineItems; } export interface CartNotifyProductAddedRequest extends XcapOptionalParameters { handle: string; variantId: string; } /** * Notify users that someone added a product to their cart * @param params */ export function cartNotifyProductAdded(params: CartNotifyProductAddedRequest): Thunk<Promise<XcapJsonResult>> { return (dispatch: any): Promise<XcapJsonResult> => { return dispatch( post({ url: '/shop/cart/notify-product-added', parameters: { ...params } }) ); }; }