UNPKG

@stackend/api

Version:

JS bindings to api.stackend.com

690 lines (589 loc) 17.2 kB
import { Checkout, UserError, Collection, GetCollectionRequest, GetCollectionResult, GetProductResult, GetProductsResult, ListProductsAndTypesResult, ListProductsRequest, ListProductTypesResult, MultipleProductListingsResult, Product, ProductVariant, SlimProduct, Country, AddressFieldName, SlimCollection, Cart } from './index'; import get from 'lodash/get'; import { ProductTypeTree, buildProductTypeTree } from './ProductTypeTree'; import { GraphQLListNode, getNextCursor, getPreviousCursor, PageInfo, GraphQLList, forEachGraphQLList, mapGraphQLList } from '../util/graphql'; import { ExtraObjectHandler, registerExtraObjectHandler } from '../api/extraObjectActions'; import { newXcapJsonResult } from '../api'; import { CustomerType, TradeRegion, VatType } from './vat'; import { CurrencyInfo } from './currency'; export const SET_SHOP_DEFAULTS = 'SET_SHOP_DEFAULTS'; export const RECEIVE_PRODUCT_TYPES = 'RECEIVE_PRODUCT_TYPES'; export const RECEIVE_PRODUCT = 'RECEIVE_PRODUCT'; export const RECEIVE_MULTIPLE_PRODUCTS = 'RECEIVE_MULTIPLE_PRODUCTS'; export const RECEIVE_LISTING = 'RECEIVE_LISTING'; export const RECEIVE_LISTINGS = 'RECEIVE_LISTINGS'; export const RECEIVE_COLLECTION = 'RECEIVE_COLLECTION'; export const RECEIVE_COLLECTIONS = 'RECEIVE_COLLECTIONS'; export const RECEIVE_COLLECTION_LIST = 'RECEIVE_COLLECTION_LIST'; export const RECEIVE_SHOPIFY_DOMAIN_REFERENCE_URL_ID = 'RECEIVE_SHOPIFY_DOMAIN_REFERENCE_URL_ID'; export const SHOP_CLEAR_CACHE = 'SHOP_CLEAR_CACHE'; export const BASKET_UPDATED = 'BASKET_UPDATED'; export const ADD_TO_BASKET = 'ADD_TO_BASKET'; export const REMOVE_FROM_BASKET = 'REMOVE_FROM_BASKET'; export const RECEIVE_CHECKOUT = 'RECEIVE_CHECKOUT'; export const CLEAR_CHECKOUT = 'CLEAR_CHECKOUT'; export const RECEIVE_COUNTRIES = 'RECEIVE_COUNTRIES'; export const RECEIVE_ADDRESS_FIELDS = 'RECEIVE_ADDRESS_FIELDS'; export const SET_VATS = 'SET_VATS'; export const SET_CUSTOMER_VAT_INFO = 'SET_CUSTOMER_VAT_INFO'; export const RECEIVE_CURRENCY = 'RECEIVE_CURRENCY'; export const RECEIVE_CART = 'RECEIVE_CART'; export const CLEAR_CART = 'CLEAR_CART'; export const SET_IS_SHOPIFY_APP = 'SET_IS_SHOPIFY_APP'; export const SET_ENABLE_CART_NOTIFICATIONS = 'SET_ENABLE_CART_NOTIFICATIONS'; export const DEFAULT_PRODUCT_TYPE = ''; export interface ShopConfig { /** Shopify domain */ domain: string; /** Storefront access token */ accessToken: string; /** Country code */ countryCode: string; /** Api version YYYY-MM */ apiVersion: string; } export interface AbstractProductListing extends PageInfo { /** Cursor not next page */ nextCursor: string | null; /** Cursor to previous page */ previousCursor: string | null; /** * The ListProductsRequest */ selection: ListProductsRequest; } export interface SlimProductListing extends AbstractProductListing { /** * Products for this page in the listing */ products: Array<SlimProduct>; } export interface VatState { /** Should the UI display prices using VAT by default? */ showPricesUsingVAT: boolean; /** Shops origin */ shopCountryCode: string; /** Country code of the customer */ customerCountryCode: string | null; /** Trade region of the customer */ customerTradeRegion: TradeRegion; /** Type of customer */ customerType: CustomerType | null; /** VAT rates as percent for the shops country */ vatRates: { [VatType.STANDARD]: number | boolean; [VatType.REDUCED]: number | boolean; [VatType.REDUCED_ALT]: number | boolean; [VatType.SUPER_REDUCED]: number | boolean; [VatType.PARKING]: number | boolean; }; /** Should vat price be shown for shipping rates? */ showVatForShipping: boolean; /** Overrides from the standard vat rate * Maps from product collection to VatType */ overrides: { [collectionHandle: string]: VatType; }; } export interface ShopDefaults { /** * Default image size for products (1024) */ imageMaxWidth: number; /** * Default image size for product listings (256) */ listingImageMaxWidth: number; /** * Default page size (20) */ pageSize: number; } export interface ShopState { /** * Default settings for the shop */ defaults: ShopDefaults; /** * Product types */ productTypes: Array<string>; /** * Product types as a tree */ productTypeTree: ProductTypeTree; /** * Products by handle */ products: { [handle: string]: Product; }; /** * Product listing arranged by getProductListKey */ productListings: { [key: string]: SlimProductListing; }; /** * Collections of products arranged by handle */ collections: { [handle: string]: Collection; }; /** * All collections as a list */ allCollections: Array<SlimCollection> | null; basketUpdated: number; /** * Current cart, if any */ cart: Cart | null; /** * Current checkout, if any */ checkout: Checkout | null; /** * Last checkout errors, if any */ checkoutUserErrors: Array<UserError> | null; /** * Country codes, or null if not loaded */ countryCodes: Array<string> | null; /** * Countries by code */ countriesByCode: { [code: string]: Country }; /** * Required Address fields by country code */ addressFieldsByCountryCode: { [code: string]: AddressFieldName[][] }; /** * Vats */ vats: VatState | null; /** * Currencies */ currencies: { [currencyCode: string]: CurrencyInfo }; /** * Reference Url id used for basket notification comments */ shopifyDomainReferenceUrlId: number; /** * Should cart notifications be enabled? (Posts a comment when someone adds a product to their cart) */ enableCartNotifications: boolean; /** * True if running as shopify app extension that requires integration with the shopify store. */ isShopifyApp: boolean; } export type SetShopDefaultsAction = { type: typeof SET_SHOP_DEFAULTS; defaults: ShopDefaults; }; export type ReceiveProductTypesAction = { type: typeof RECEIVE_PRODUCT_TYPES; json: ListProductTypesResult; }; export type ReceiveProductAction = { type: typeof RECEIVE_PRODUCT; json: GetProductResult; }; export type ReceiveMultipleProductsAction = { type: typeof RECEIVE_MULTIPLE_PRODUCTS; json: GetProductsResult; }; export type ReceiveListingAction = { type: typeof RECEIVE_LISTING; json: ListProductsAndTypesResult; key: string; request: ListProductsRequest; }; export type ReceiveListingsAction = { type: typeof RECEIVE_LISTINGS; listings: MultipleProductListingsResult; }; export type ReceiveCollectionAction = { type: typeof RECEIVE_COLLECTION; json: GetCollectionResult; request: GetCollectionRequest; }; export type ReceiveCollectionsAction = { type: typeof RECEIVE_COLLECTIONS; collections: { [handle: string]: Collection }; }; export type ReceiveCollectionListAction = { type: typeof RECEIVE_COLLECTION_LIST; collections: GraphQLList<SlimCollection>; }; export type ReceiveShopifyDomainReferenceUrlId = { type: typeof RECEIVE_SHOPIFY_DOMAIN_REFERENCE_URL_ID; shopifyDomainReferenceUrlId: number; }; export type ClearCacheAction = { type: typeof SHOP_CLEAR_CACHE; }; export type ClearCartAction = { type: typeof CLEAR_CART; }; export type ReceiveCartAction = { type: typeof RECEIVE_CART; cart: Cart | null; }; export type AddToBasketAction = { type: typeof ADD_TO_BASKET; product: Product; variantId: string; variant: ProductVariant; quantity: number; }; export type RemoveFromBasketAction = { type: typeof REMOVE_FROM_BASKET; product: Product; variant: ProductVariant; variantId: string; quantity: number; }; export type BasketUpdatedAction = { type: typeof BASKET_UPDATED; }; export type ReceiveCheckoutAction = { type: typeof RECEIVE_CHECKOUT; checkoutUserErrors: Array<UserError> | null; checkout: Checkout; }; export type ClearCheckoutAction = { type: typeof CLEAR_CHECKOUT; }; export type ReceiveCountriesAction = { type: typeof RECEIVE_COUNTRIES; countries: Array<Country>; }; export type ReceiveAddressFieldsAction = { type: typeof RECEIVE_ADDRESS_FIELDS; countryCode: string; addressFields: AddressFieldName[][]; }; export type SetVATsAction = { type: typeof SET_VATS; vats: VatState; }; export type SetCustomerVATInfoAction = { type: typeof SET_CUSTOMER_VAT_INFO; customerCountryCode?: string; customerTradeRegion?: TradeRegion; customerType?: CustomerType; }; export type ReceiveCurrencyAction = { type: typeof RECEIVE_CURRENCY; currency: CurrencyInfo; }; export type SetIsShopifyAppAction = { type: typeof SET_IS_SHOPIFY_APP; shopifyApp: boolean; }; export type SetEnableCartNotificationsAction = { type: typeof SET_ENABLE_CART_NOTIFICATIONS; enableCartNotifications: boolean; }; export type ShopActions = | SetShopDefaultsAction | ReceiveProductTypesAction | ReceiveProductAction | ReceiveMultipleProductsAction | ReceiveListingAction | ReceiveListingsAction | ReceiveCollectionAction | ReceiveCollectionsAction | ReceiveCollectionListAction | ReceiveShopifyDomainReferenceUrlId | ClearCacheAction | AddToBasketAction | RemoveFromBasketAction | BasketUpdatedAction | ReceiveCheckoutAction | ClearCheckoutAction | ReceiveCountriesAction | ReceiveAddressFieldsAction | SetVATsAction | SetCustomerVATInfoAction | ReceiveCurrencyAction | ClearCartAction | ReceiveCartAction | SetIsShopifyAppAction | SetEnableCartNotificationsAction; export default function shopReducer( state: ShopState = { defaults: { imageMaxWidth: 1024, listingImageMaxWidth: 256, pageSize: 20 }, productTypes: [], productTypeTree: [], products: {}, productListings: {}, collections: {}, allCollections: null, basketUpdated: 0, cart: null, checkout: null, checkoutUserErrors: null, countryCodes: null, countriesByCode: {}, addressFieldsByCountryCode: {}, vats: null, currencies: {}, enableCartNotifications: false, shopifyDomainReferenceUrlId: 0, isShopifyApp: false }, action: ShopActions ): ShopState { switch (action.type) { case SET_SHOP_DEFAULTS: return Object.assign({}, state, { defaults: action.defaults, // Clear the cache as well productListings: {}, products: {}, productTypes: [], productTypeTree: [], collections: {}, allCollections: null, countryCodes: null, countriesByCode: {}, addressFieldsByCountryCode: {}, enableCartNotifications: false, shopifyDomainReferenceUrlId: 0, isShopifyApp: false }); case RECEIVE_PRODUCT_TYPES: { const edges: Array<GraphQLListNode<string>> = get(action, 'json.productTypes.edges', []); const productTypes = edges.map(e => e.node); return Object.assign({}, state, { productTypes, productTypeTree: buildProductTypeTree(edges) }); } case RECEIVE_PRODUCT: { const product = get(action, 'json.product'); if (product) { const products = Object.assign({}, state.products, { [product.handle]: product }); return Object.assign({}, state, { products }); } break; } case RECEIVE_MULTIPLE_PRODUCTS: { const receivedProducts = get(action, 'json.products'); if (receivedProducts) { const products = Object.assign({}, state.products, receivedProducts); return Object.assign({}, state, { products }); } break; } case RECEIVE_LISTING: { const receivedProducts = action.json.products; const listing: SlimProductListing = { hasNextPage: receivedProducts.pageInfo.hasNextPage, hasPreviousPage: receivedProducts.pageInfo.hasPreviousPage, nextCursor: getNextCursor(receivedProducts), previousCursor: getPreviousCursor(receivedProducts), selection: action.request, products: [] }; receivedProducts.edges.forEach(n => { listing.products.push(n.node); }); const productListings = Object.assign({}, state.productListings, { [action.key]: listing }); return Object.assign({}, state, { productListings }); } case RECEIVE_LISTINGS: { const listings: { [key: string]: SlimProductListing } = {}; for (const key of Object.keys(action.listings)) { const { listing, request } = action.listings[key]; const l: SlimProductListing = { hasNextPage: listing.pageInfo.hasNextPage, hasPreviousPage: listing.pageInfo.hasPreviousPage, nextCursor: getNextCursor(listing), previousCursor: getPreviousCursor(listing), selection: request, products: mapGraphQLList(listing, (e: SlimProduct) => e) }; listings[key] = l; } return Object.assign({}, state, { productListings: Object.assign({}, state.productListings, listings) }); } case RECEIVE_COLLECTION: { const collection = action.json.collection; const handle = action.request.handle; return Object.assign({}, state, { collections: Object.assign({}, state.collections, { [handle]: collection }) }); } case RECEIVE_COLLECTIONS: { return Object.assign({}, state, { collections: Object.assign({}, state.collections, action.collections) }); } case RECEIVE_COLLECTION_LIST: { const allCollections: Array<SlimCollection> = []; forEachGraphQLList<SlimCollection>(action.collections, item => { allCollections.push(item); }); return Object.assign({}, state, { allCollections }); } case RECEIVE_SHOPIFY_DOMAIN_REFERENCE_URL_ID: { return Object.assign({}, state, { shopifyDomainReferenceUrlId: action.shopifyDomainReferenceUrlId }); } case SHOP_CLEAR_CACHE: return Object.assign({}, state, { productListings: {}, products: {}, productTypes: [], productTypeTree: [], collections: {}, allCollections: null, countryCodes: null, countriesByCode: {}, addressFieldsByCountryCode: {} }); case BASKET_UPDATED: case ADD_TO_BASKET: case REMOVE_FROM_BASKET: return Object.assign({}, state, { basketUpdated: Date.now() }); case RECEIVE_CART: { return Object.assign({}, state, { cart: action.cart }); } case CLEAR_CART: { return Object.assign({}, state, { cart: null }); } case RECEIVE_CHECKOUT: { return Object.assign({}, state, { checkout: action.checkout, checkoutUserErrors: action.checkoutUserErrors }); } case CLEAR_CHECKOUT: return Object.assign({}, state, { checkout: null }); case RECEIVE_COUNTRIES: { const countryCodes: Array<string> = []; const countriesByCode: { [code: string]: Country } = {}; action.countries.forEach(c => { countryCodes.push(c.code); countriesByCode[c.code] = c; }); return Object.assign({}, state, { countryCodes, countriesByCode }); } case RECEIVE_ADDRESS_FIELDS: { const addressFieldsByCountryCode = Object.assign({}, state.addressFieldsByCountryCode, { [action.countryCode]: action.addressFields }); return Object.assign({}, state, { addressFieldsByCountryCode }); } case SET_VATS: { return Object.assign({}, state, { vats: Object.assign({}, state.vats, action.vats) }); } case SET_CUSTOMER_VAT_INFO: return Object.assign({}, state, { vats: Object.assign({}, state.vats || {}, { customerCountryCode: action.customerCountryCode || state.vats?.customerCountryCode, customerTradeRegion: action.customerTradeRegion || state.vats?.customerTradeRegion, customerType: action.customerType || state.vats?.customerType }) }); case RECEIVE_CURRENCY: return Object.assign({}, state, { currencies: { ...state.currencies, [action.currency.code]: action.currency } }); case SET_IS_SHOPIFY_APP: return Object.assign({}, state, { isShopifyApp: action.shopifyApp || false }); case SET_ENABLE_CART_NOTIFICATIONS: return Object.assign({}, state, { enableCartNotifications: action.enableCartNotifications || false }); } return state; } const PRODUCT_REFERENCE_HANDLER: ExtraObjectHandler<Product> = { key: 'products', context: 'shop', onExtraObjectsReceived: (objects, dispatch) => { const products: { [handle: string]: Product } = objects as any; const json = newXcapJsonResult<GetProductsResult>('success', { products }); dispatch({ type: RECEIVE_MULTIPLE_PRODUCTS, json }); } }; // If this reducer is used, register its reference handler registerExtraObjectHandler(PRODUCT_REFERENCE_HANDLER);