import { combineReducers } from 'redux'; import * as constants from './constants'; import { isSameProduct } from './selectors'; import { stringifyFrequency } from './api'; import { getObjectStructuredProductPlans } from './adapters'; import { safeProductId, getMatchingProductIfExists } from './utils'; import { experimentsReducer } from './experiments'; import { AuthState, EnvironmentState, AutoshipByDefaultState, AutoshipEligibleState, BenefitMessagesState, ConfigState, Incentive, IncentiveObject, IncentivesState, NextUpcomingOrderState, OptedInState, OptedOutState, PrepaidShipmentsSelectedState, PriceState, ProductPlansState, ReceiveOfferPayload } from './types/reducer'; import { EmptyObject } from './types/utility'; import { IncentiveDisplay, IncentivesDisplayEnhanced } from './types/api'; export const optedin = (state: OptedInState = [], action): OptedInState => { switch (action.type) { case constants.LOCAL_STORAGE_CLEAR: return []; case constants.LOCAL_STORAGE_CHANGE: return action.newValue ? action.newValue.optedin : state; case constants.OPTIN_PRODUCT: case constants.PRODUCT_CHANGE_FREQUENCY: { // since prepaid maps to a different set of frequencies, we remove prepaidShipments when frequency is changed const [{ prepaidShipments, ...oldone }, rest] = getMatchingProductIfExists(state, action.payload.product); return rest.concat({ ...oldone, ...action.payload.product, frequency: action.payload.frequency }); } case constants.PRODUCT_CHANGE_PREPAID_SHIPMENTS: { const { payload } = action; const [{ prepaidShipments, ...oldone }, rest] = getMatchingProductIfExists(state, payload.product); const newState = { ...oldone, ...payload.product }; if (payload.prepaidShipments) { newState.prepaidShipments = payload.prepaidShipments; } return rest.concat(newState); } case constants.OPTOUT_PRODUCT: return state.filter(a => !isSameProduct(action.payload.product, a)); case constants.PRODUCT_HAS_CHANGED: return state.map(product => isSameProduct(action.payload.product, product) ? { ...product, ...action.payload.newProduct } : product ); case constants.CONVERT_ONE_TIME: return state.filter(a => !isSameProduct(action.payload.product, a)); case constants.CHECKOUT: return []; default: return state; } }; export const optedout = (state: OptedOutState = [], action): OptedOutState => { switch (action.type) { case constants.LOCAL_STORAGE_CLEAR: return []; case constants.LOCAL_STORAGE_CHANGE: return action.newValue ? action.newValue.optedout : state; case constants.OPTIN_PRODUCT: case constants.PRODUCT_CHANGE_FREQUENCY: return state.filter(a => !isSameProduct(action.payload.product, a)); case constants.OPTOUT_PRODUCT: { const [oldone, rest] = getMatchingProductIfExists(state, action.payload.product); return rest.concat({ ...oldone, ...action.payload.product, frequency: action.payload.frequency }); } case constants.PRODUCT_HAS_CHANGED: return state.map(product => isSameProduct(action.payload.product, product) ? { ...product, ...action.payload.newProduct } : product ); case constants.CHECKOUT: return []; default: return state; } }; export const nextUpcomingOrder = (state: NextUpcomingOrderState = {}, { type, payload }): NextUpcomingOrderState => { switch (type) { case constants.RECEIVE_ORDERS: return payload && payload.count > 0 ? { ...state, ...(payload.results[0] && { ...payload.results[0], place: new Date(Date.parse(payload.results[0].place.replace(/-/gi, '/'))) }) } : state; case constants.RECEIVE_ORDER_ITEMS: return { ...state, products: (payload.results || []).map(it => it.product) }; case constants.CREATE_ONE_TIME: // when CREATE_IU_ORDER payload is just order item object created return { ...state, ...payload, public_id: payload.order, ...(payload.product && { products: (state.products || []).concat(payload.product) }) }; default: return state; } }; export const autoshipEligible = (state: AutoshipEligibleState = {}, action): AutoshipEligibleState => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...action.payload.autoship }; default: return state; } }; export const inStock = (state = {}, action) => { switch (action.type) { // force offer to refresh when requesting a new one case constants.REQUEST_OFFER: return { ...state }; case constants.RECEIVE_OFFER: return { ...state, ...action.payload.in_stock }; default: return state; } }; export const eligibilityGroups = (state = {}, action) => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...action.payload.eligibility_groups }; default: return state; } }; const mapIncentive = ( incentive: string[], incentiveDisplay: IncentiveDisplay, incentiveDisplayEnhanced?: IncentivesDisplayEnhanced ): Incentive[] => { return incentive.map(i => { const enhanced = incentiveDisplayEnhanced?.[i]; return { ...incentiveDisplay[i], // for standard incentives, include the criteria so we know which kind of incentive (e.g. PSI, prepaid, etc) ...(enhanced ? { enhanced: true, criteria: enhanced.criteria ? enhanced.criteria : // when there is no criteria in the enhanced incentive, it means it's a program wide incentive // for ease-of-use, we set use a "PROGRAM_WIDE" pseudo-standard here { node_type: 'PREMISE', standard: constants.INCENTIVE_STANDARD_TYPES.PROGRAM_WIDE, premise_value: null }, threshold_field: enhanced.threshold_field, threshold_value: enhanced.threshold_value } : {}), id: [i][0] }; }); }; export const incentives = ( state: IncentivesState = {}, action: { type: string; payload: ReceiveOfferPayload } ): IncentivesState => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...[...new Set(Object.keys(action.payload.incentives || {}))].reduce( (obj, uniqueProductId) => ({ ...obj, [uniqueProductId]: Object.entries(action.payload.incentives) .filter(([productId]) => productId === uniqueProductId) .reduce( (incentiveObj: IncentiveObject | EmptyObject, [, { initial, ongoing }]) => ({ ...incentiveObj, initial: [ ...(incentiveObj.initial || []), ...mapIncentive( initial, action.payload.incentives_display, action.payload.incentives_display_enhanced ) ], ongoing: [ ...(incentiveObj.ongoing || []), ...mapIncentive( ongoing, action.payload.incentives_display, action.payload.incentives_display_enhanced ) ] }), {} ) }), {} ) }; default: return state; } }; export const frequency = (state = {}, action) => { switch (action.type) { case constants.OPTIN_PRODUCT: case constants.PRODUCT_CHANGE_FREQUENCY: return { ...state, [safeProductId(action.payload.product)]: action.payload.frequency }; case constants.OPTOUT_PRODUCT: return { ...state, [safeProductId(action.payload.product)]: undefined }; default: return state; } }; export const auth = (state: AuthState = false, action): AuthState => { switch (action.type) { case constants.AUTHORIZE: return { ...action.payload }; case constants.UNAUTHORIZED: return false; default: return state; } }; export const merchantId = (state = '', action) => { switch (action.type) { case constants.SET_MERCHANT_ID: return action.payload; default: return state; } }; export const authUrl = (state = null, action) => { switch (action.type) { case constants.SET_AUTH_URL: return action.payload; default: return state; } }; export const offer = (state = {}, action) => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, offerId: (action.payload.module_view || {}).regular, ...action.payload.modifiers }; default: return state; } }; export const offerId = (state = '', action) => { switch (action.type) { case constants.RECEIVE_OFFER: return (action.payload.module_view || {}).regular || ''; default: return state; } }; export const sessionId = (state = null, action) => { switch (action.type) { case constants.LOCAL_STORAGE_CLEAR: return null; case constants.CREATED_SESSION_ID: return action.payload; default: return state; } }; export const productOffer = (state = {}, action) => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...Object.entries(action.payload.autoship) .map(([key]) => ({ [key]: Object.keys(action.payload.modifiers) })) .reduce((acc, object) => ({ ...acc, ...object }), {}) }; case constants.CHECKOUT: return {}; default: return state; } }; export const firstOrderPlaceDate = (state = {}, action) => { switch (action.type) { case constants.SET_FIRST_ORDER_PLACE_DATE: return { ...state, [safeProductId(action.payload.product)]: action.payload.firstOrderPlaceDate }; default: return state; } }; export const productToSubscribe = (state = {}, action) => { switch (action.type) { case constants.SET_PRODUCT_TO_SUBSCRIBE: return { ...state, [safeProductId(action.payload.product)]: action.payload.productToSubscribe }; default: return state; } }; export const environment = (state: EnvironmentState = {}, action): EnvironmentState => { switch (action.type) { case constants.SET_ENVIRONMENT_LOCAL: return { ...state, name: 'local', apiUrl: 'http://py3web.ordergroove.localhost', legoUrl: 'http://py3lego.ordergroove.localhost' }; case constants.SET_ENVIRONMENT_STAGING: return { ...state, name: constants.ENV_STAGING, apiUrl: 'https://staging.offers.ordergroove.com', // scUrl: 'https://staging.sc.ordergroove.com', // widgetsUrl: 'https://staging.static.ordergroove.com', // masterDbUrl: 'https://staging.v2.ordergroove.com', // reorderUrl: 'https://staging.static.ordergroove.com/reorder/', legoUrl: 'https://staging.restapi.ordergroove.com' }; case constants.SET_ENVIRONMENT_DEV: return { ...state, name: constants.ENV_DEV, apiUrl: 'https://dev.offers.ordergroove.com', // scUrl: 'https://dev.sc.ordergroove.com', // widgetsUrl: 'https://dev.static.ordergroove.com', // masterDbUrl: 'https://dev.api.ordergroove.com', // reorderUrl: 'https://staging.static.ordergroove.com/reorder/', legoUrl: 'https://dev.restapi.ordergroove.com' }; case constants.SET_ENVIRONMENT_PROD: return { ...state, name: constants.ENV_PROD, apiUrl: 'https://offers.ordergroove.com', // scUrl: 'https://sc.ordergroove.com', // widgetsUrl: 'https://static.ordergroove.com', // masterDbUrl: 'https://api.ordergroove.com', // reorderUrl: 'https://static.ordergroove.com/reorder/', legoUrl: 'https://restapi.ordergroove.com' }; default: return state; } }; export const locale = ( state = { offerOptInLabel: 'Subscribe to save', offerIncentiveText: 'Save {{ogIncentive DiscountPercent}} when you subscribe', offerOptOutLabel: 'Deliver one-time only', offerEveryLabel: 'Delivery Every', offerTooltipTrigger: '[?]', offerTooltipContent: 'Seems this is a great subscription offering. Many fun details about this program exist.', optinButtonLabel: '•', optoutButtonLabel: '•', optinStatusOptedInLabel: "You're opted in!", optinStatusOptedOutLabel: "You're not opted in.", optinToggleLabel: '•', upsellButtonLabel: 'Add item to order on ', upsellButtonPrefix: '', upsellModalContent: 'Some upsell modal content', upsellModalOptInLabel: 'Subscribe', upsellModalOptOutLabel: 'Purchase one time', upsellModalTitle: 'Impulse Upsell', upsellModalConfirmLabel: 'Ok', upsellModalCancelLabel: 'Cancel', defaultFrequencyCopy: '(Most Popular)', frequencyPeriods: { 1: 'day', 2: 'week', 3: 'month' }, prepaidOptInLabel: 'Prepaid Subscription', prepaidShipmentsLabel: 'Number of prepaid shipments' }, action ) => { switch (action.type) { case constants.SET_LOCALE: return { ...state, ...action.payload }; default: return state; } }; export const config = ( state: ConfigState = { offerType: 'radio' }, action ): ConfigState => { switch (action.type) { case constants.SET_CONFIG: return { ...state, ...action.payload, // these are not populated by default; only if the merchant calls the config method on the Offers API defaultFrequency: action.payload.defaultFrequency ? stringifyFrequency(action.payload.defaultFrequency) : state.defaultFrequency, frequenciesEveryPeriod: [], frequencies: action.payload.frequencies ? action.payload.frequencies.map(stringifyFrequency) : state.frequencies }; case constants.RECEIVE_MERCHANT_SETTINGS: return { ...state, merchantSettings: { ...action.payload } }; default: return state; } }; export const previewStandardOffer = (state = false, action) => { switch (action.type) { case constants.SET_PREVIEW_STANDARD_OFFER: return action.payload.isPreview; default: return state; } }; export const previewUpsellOffer = (state = false, action) => { switch (action.type) { case constants.SET_PREVIEW_UPSELL_OFFER: return action.payload.isPreview; default: return state; } }; export const previewPrepaidOffer = (state = false, action) => { switch (action.type) { case constants.SET_PREVIEW_PREPAID_OFFER: return action.payload.isPreview; default: return state; } }; export const autoshipByDefault = (state: AutoshipByDefaultState = {}, action): AutoshipByDefaultState => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...action.payload.autoship_by_default }; default: return state; } }; export const defaultFrequencies = (state = [], action) => { switch (action.type) { case constants.RECEIVE_OFFER: return { ...state, ...action.payload.default_frequencies }; default: return state; } }; export const templates = (state = [], action) => { switch (action.type) { case constants.SET_TEMPLATES: return [...(action.payload || [])]; case constants.ADD_TEMPLATE: // Unshift the payload at the top return [action.payload, ...state]; default: return state; } }; export const productPlans = (state: ProductPlansState = {}, action): ProductPlansState => { switch (action.type) { case constants.RECEIVE_PRODUCT_PLANS: return getObjectStructuredProductPlans(action.payload); default: return state; } }; export const prepaidShipmentsSelected = ( state: PrepaidShipmentsSelectedState = {}, action ): PrepaidShipmentsSelectedState => { switch (action.type) { // Given that, in the cart, products will have a composed id (:) and that every time // a product changes in the cart we need to sync these changes back with the eComm platform, this operation // may result in a new cartId that needs to replace the old product's cartId in order to have the // prepaidShipmentsSelected object up-to-date case constants.CART_PRODUCT_KEY_HAS_CHANGED: { const { [action.payload.oldCartProductKey]: preservedPrepaidShipments, ...stateWithoutOldCartProductKey } = state; return { ...stateWithoutOldCartProductKey, [action.payload.newCartProductKey]: preservedPrepaidShipments }; } case constants.PRODUCT_CHANGE_PREPAID_SHIPMENTS: if (action.payload.prepaidShipments) { return { ...state, [action.payload.product.id]: action.payload.prepaidShipments }; } return state; default: return state; } }; export const price = (state: PriceState = {}, _action) => state; export const benefitMessages = (state: BenefitMessagesState = {}, action): BenefitMessagesState => { switch (action.type) { case constants.SET_BENEFIT_MESSAGES: return { ...(action.payload || {}) }; default: return state; } }; export default combineReducers({ optedin, optedout, nextUpcomingOrder, autoshipEligible, inStock, eligibilityGroups, incentives, frequency, auth, merchantId, authUrl, offer, offerId, experiments: experimentsReducer, sessionId, productOffer, firstOrderPlaceDate, productToSubscribe, environment, locale, config, previewStandardOffer, previewUpsellOffer, autoshipByDefault, defaultFrequencies, templates, productPlans, prepaidShipmentsSelected, price, benefitMessages });