Commit f63614f5 authored by Denis Sedura's avatar Denis Sedura

Add actions, configs, constans, middlwares, reducers, selector and utils from tipsi-react-app

Add AgeConfirmation
parent e7958dce
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { createSelector } from 'reselect'
import { checkApiVersion } from 'tipsi_api/actions'
import { checkApiSelector } from 'tipsi_api/selectors'
import { persistorRehydration } from '../../store'
import { updateAgeConfirmation } from '../../actions'
import { userAgeConfirmationSelector } from '../../selectors'
function enhanceAgeConfirmation(ComposedComponent) {
class Wrapper extends Component {
static propTypes = {
ageConfirmation: PropTypes.bool.isRequired,
updateAgeConfirmation: PropTypes.func.isRequired,
onComplete: PropTypes.func,
}
static defaultProps = {
onComplete: () => {},
}
state = {
userRejectedAge: false,
storeLoaded: false,
}
async componentWillMount() {
await persistorRehydration
this.setState({ storeLoaded: true })
if (this.props.ageConfirmation) {
this.props.onComplete()
}
}
handleRequestClose = () => {}
handleAgreeAge = () => {
this.props.updateAgeConfirmation(true)
this.props.onComplete()
}
handleRejectAge = () => {
this.setState({ userRejectedAge: true })
}
render() {
return (
<ComposedComponent
{...this.props}
userRejectedAge={this.state.userRejectedAge}
storeLoaded={this.state.storeLoaded}
handleRequestClose={this.handleRequestClose}
handleAgreeAge={this.handleAgreeAge}
handleRejectAge={this.handleRejectAge}
/>
)
}
}
return connect(userAgeConfirmationSelector, { updateAgeConfirmation })(Wrapper)
}
export default enhanceAgeConfirmation
export { default as enhanceAgeConfirmation } from './AgeConfirmation'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { createSelector } from 'reselect'
import { checkApiVersion } from 'tipsi_api/actions'
import { checkApiSelector } from 'tipsi_api/selectors'
......@@ -16,7 +17,7 @@ function enhanceInitialComponent(ComposedComponent) {
render() {
return (
<ComposedComponent {...this.props} test={this.test}/>
<ComposedComponent {...this.props} />
)
}
}
......
export { enhanceInitialComponent } from './InitialScreen'
export { enhanceAgeConfirmation } from './AgeConfirmation'
export const CLEAN_REDUCERS = 'CLEAN_REDUCERS'
export function cleanReducers(reducers) {
return { type: CLEAN_REDUCERS, payload: { reducers } }
}
export default function createAsyncAction(types, callback) {
return payload => async (dispatch) => {
const [requestType, successType, failureType] = types
dispatch({ type: requestType, payload })
try {
await callback(dispatch, payload)
} catch (error) {
return dispatch({ type: failureType, payload: { ...payload, error } })
}
return dispatch({ type: successType, payload })
}
}
export const UPDATE_CURRENT_LOCATION = 'UPDATE_CURRENT_LOCATION'
export function updateCurrentLocation(payload) {
return { type: UPDATE_CURRENT_LOCATION, payload }
}
export * from './sidebarMenu'
export * from './currentLocation'
export * from './searchQueries'
export * from './storeFilters'
export * from './storeCarts'
export * from './userCards'
export * from './userAddresses'
export * from './payments'
export * from './userAgeConfirmation'
export * from './cleanReducers'
import stripe from 'tipsi-stripe'
import { get } from 'lodash'
import { performPayment, loadOrder, updateOrder } from 'tipsi_api/actions'
import { createOrderSelector } from 'tipsi_api/selectors'
import { createAsyncConstants } from 'tipsi_api/utils'
import { APPLE_PAY, ANDROID_PAY } from '../constants/paymentMethods'
import { repeat, geometricPause } from '../utils/sequentional'
import { applePayOrderFormatter, androidPayOrderFormatter } from '../utils/formatters'
export const PaymentsActionTypes = createAsyncConstants(['REQUEST_PAYMENT', 'FINALIZE_PAYMENT'])
const TIMES_TO_REPEAT = 10
const PAUSE_AFTER_REPEAT = geometricPause(300)
const TIMEOUT_ERROR_TEXT = 'Error while placing order, try again in 30 sec.'
const ORDER_FIELDS = {
order: 'id,created,updated,code,order_status,payment_status,store,total_price,total_count',
}
/**
* returns true if we need to check order status again
*/
const checkOrderStatus = orderId => ({ payload }) => {
const order = payload.entities.orders[orderId]
if (order.payment_error) {
throw new Error(order.payment_error)
} else if (order.order_status !== 'placed') {
throw new Error('Error while placing order')
} else if (order.payment_status !== 'held') {
return true
}
return false
}
function generatePaymentToken(order, method, card) {
if (method === APPLE_PAY) {
const { items, options } = applePayOrderFormatter(order)
return stripe.paymentRequestWithApplePay(items, options)
}
if (method === ANDROID_PAY) {
return stripe.paymentRequestWithAndroidPay(androidPayOrderFormatter(order))
}
return stripe.createTokenWithCard(card)
}
export const requestPayment = (orderId, deliveryType) => async (dispatch, getState) => {
try {
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.LOADING, payload: { orderId } })
// Load order from REDUX
const order = createOrderSelector(() => orderId)(getState()).order
// Update order
if (order.order_status !== 'placed') {
await dispatch(updateOrder(orderId, { order_status: 'placed', delivery_type: deliveryType }))
}
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.SUCCESS, payload: { orderId } })
} catch (error) {
dispatch({ type: PaymentsActionTypes.REQUEST_PAYMENT.ERROR, payload: { orderId, error } })
throw error
}
}
export const finalizePayment = (orderId, storeId, method, card) => async (dispatch, getState) => {
try {
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.LOADING, payload: { orderId } })
// Load order from REDUX
const order = createOrderSelector(() => orderId)(getState()).order
// Generate payment token
const paymentResult = await generatePaymentToken(order, method, card)
// Update order
const phone = get(paymentResult, 'extra.shippingContact.phoneNumber')
if (phone) {
await dispatch(updateOrder(orderId, { phone }))
}
// Perform payment
const { tokenId: paymentTokenId } = paymentResult
await dispatch(performPayment(storeId, orderId, {
provider: 'stripe',
usd_amount: order.total_price,
data: { source: paymentTokenId },
}))
// Check order status
await repeat({
perform: () => dispatch(loadOrder({ orderId, fields: ORDER_FIELDS })),
until: checkOrderStatus(orderId),
times: TIMES_TO_REPEAT,
pause: PAUSE_AFTER_REPEAT,
error: TIMEOUT_ERROR_TEXT,
})
// Complete Apple Pay request
if (method === APPLE_PAY) {
await stripe.completeApplePayRequest()
}
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.SUCCESS, payload: { orderId } })
} catch (error) {
// Cancel Apple Pay request
if (method === APPLE_PAY) {
const { TPSErrorDomain, TPSErrorCodeUserCancel } = stripe
const userCanceled = error.domain === TPSErrorDomain && error.code === TPSErrorCodeUserCancel
if (!userCanceled) {
await stripe.cancelApplePayRequest()
}
}
dispatch({ type: PaymentsActionTypes.FINALIZE_PAYMENT.ERROR, payload: { orderId, error } })
throw error
}
}
export const CHANGE_SEARCH_QUERY = 'CHANGE_SEARCH_QUERY'
export const CLEAR_SEARCH_QUERY = 'CLEAR_SEARCH_QUERY'
export const GLOBAL_INVENTORY_SEARCH_KEY = 'globalInventorySearch'
export const GLOBAL_SEARCH_KEY = 'globalSearch'
export const MAIN_SEARCH_KEY = 'mainSearch'
export const STORE_SEARCH_KEY = 'storeSearch'
export function changeSearchQuery({ key, query }) {
return {
type: CHANGE_SEARCH_QUERY,
payload: {
key,
query,
},
}
}
export function clearSearchQuery({ keys }) {
return {
type: CLEAR_SEARCH_QUERY,
payload: {
keys,
},
}
}
export const OPEN_SIDEBAR_MENU = 'OPEN_SIDEBAR_MENU'
export const CLOSE_SIDEBAR_MENU = 'CLOSE_SIDEBAR_MENU'
export function openSidebarMenu() {
return { type: OPEN_SIDEBAR_MENU }
}
export function closeSidebarMenu() {
return { type: CLOSE_SIDEBAR_MENU }
}
import { get, omit } from 'lodash'
import {
createStoreSelector,
createOrderSelector,
createStoreCartOrderSelector,
createOrderProductsSelector,
createInventoryOrderProductSelector,
} from 'tipsi_api/selectors'
import {
loadStore,
loadStoreCartOrder,
createOrder,
addProductToOrder,
loadOrder,
updateProductCount,
removeProduct,
loadProducts,
} from 'tipsi_api/actions'
import { createAsyncConstants } from 'tipsi_api/utils'
import createAsyncAction from './createAsyncAction'
import { defaultDeliveryType } from '../utils/deliveryTypes'
export const ActionTypes = createAsyncConstants([
'LOAD_STORE_CART',
'ADD_INVENTORY_TO_CART',
'INCREASE_INVENTORY_COUNT',
'DECREASE_INVENTORY_COUNT',
'REMOVE_INVENTORY_FROM_CART',
])
export const loadStoreCart = createAsyncAction(
ActionTypes.LOAD_STORE_CART,
async (dispatch, { storeId }) => {
await dispatch(loadStoreIfNeeded(storeId))
await dispatch(loadOrderIfNeeded(storeId))
await dispatch(loadOrderProductsIfNeeded(storeId))
}
)
export const addInventoryToCart = createAsyncAction(
ActionTypes.ADD_INVENTORY_TO_CART,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
await dispatch(loadOrderIfNeeded(storeId))
const order = await dispatch(createOrderIfNeeded(storeId))
await dispatch(addInventoryToCartIfNeeded(storeId, inventoryId))
await dispatch(loadOrderProductsIfPossible(storeId))
await dispatch(reloadOrder(order.id))
}
)
export const increaseInventoryCount = createAsyncAction(
ActionTypes.INCREASE_INVENTORY_COUNT,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(increaseInventoryCountIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
export const decreaseInventoryCount = createAsyncAction(
ActionTypes.DECREASE_INVENTORY_COUNT,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(decreaseInventoryCountIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
export const removeInventoryFromCart = createAsyncAction(
ActionTypes.REMOVE_INVENTORY_FROM_CART,
async (dispatch, { storeId, inventoryId }) => {
await dispatch(loadStoreIfNeeded(storeId))
const order = await dispatch(loadOrderIfNeeded(storeId))
await dispatch(removeInventoryFromCartIfNeeded(storeId, inventoryId))
await dispatch(reloadOrder(order.id))
}
)
function lookupStoreById(state, storeId) {
return createStoreSelector(() => storeId)(state).store
}
function lookupOrderById(state, orderId) {
return createOrderSelector(() => orderId)(state).order
}
function lookupOrder(state, storeId) {
return createStoreCartOrderSelector(() => storeId)(state)
}
function lookupOrderProduct(state, storeId, inventoryId) {
const order = lookupOrder(state, storeId) || {}
return createInventoryOrderProductSelector(() => order.id, () => inventoryId)(state)
}
function lookupOrderProducts(state, storeId) {
const order = lookupOrder(state, storeId) || {}
return createOrderProductsSelector(() => order.id)(state).orderProducts
}
const loadStoreIfNeeded = storeId => async (dispatch, getState) => {
let store = lookupStoreById(getState(), storeId)
if (!store || store.orders_enabled === undefined) {
await dispatch(loadStore(storeId))
store = lookupStoreById(getState(), storeId)
}
return store
}
const loadOrderIfNeeded = storeId => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
let order = lookupOrder(state, storeId)
if (store && store.orders_enabled && !order) {
await dispatch(loadStoreCartOrder({
storeId,
fields: {
order: 'id,store,total_count',
store: 'id,orders_enabled',
},
}))
order = lookupOrder(getState(), storeId)
}
return order
}
const loadOrderProductsIfPossible = storeId => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProducts = []
if (store && store.orders_enabled && order) {
let page = 1
let shouldLoadNextPage = true
while (shouldLoadNextPage) {
const resultAction = await dispatch(loadProducts({
orderId: order.id,
fields: {
product: 'id,count,total_price,wine,drink,inventory',
inventory: 'id,price,special_price_on,special_price,special_price_amount,in_stock',
wine: 'id,name,vintage,label_url',
drink: 'id,name,label_url',
},
page,
}))
page += 1
shouldLoadNextPage = !!get(resultAction, 'payload.result.next')
}
orderProducts = lookupOrderProducts(getState(), storeId)
}
return orderProducts
}
const loadOrderProductsIfNeeded = storeId => async (dispatch, getState) => {
let orderProducts = lookupOrderProducts(getState(), storeId)
if (orderProducts && orderProducts.length === 0) {
await dispatch(loadOrderProductsIfPossible(storeId))
orderProducts = lookupOrderProducts(getState(), storeId)
}
return orderProducts
}
let createOrderPromises = {}
const createOrderIfNeeded = storeId => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
let order = lookupOrder(state, storeId)
if (store && store.orders_enabled && !order) {
let createOrderPromise = createOrderPromises[storeId]
if (!createOrderPromise) {
createOrderPromise = dispatch(createOrder({
store_id: storeId,
delivery_type: defaultDeliveryType,
}))
createOrderPromises[storeId] = createOrderPromise
}
await createOrderPromise
createOrderPromises = omit(createOrderPromises, storeId)
order = lookupOrder(getState(), storeId)
}
return order
}
const addInventoryToCartIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct
if (store && store.orders_enabled && order) {
await dispatch(addProductToOrder({
orderId: order.id,
data: {
inventory_id: inventoryId,
count: 1,
},
}))
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const reloadOrder = orderId => async (dispatch, getState) => {
await dispatch(loadOrder({
orderId,
fields: {
order: 'id,total_count,order_status,' +
'products_price,products_tax,products_discount,delivery_price,delivery_tax,total_price,' +
'store',
store: 'id,name,orders_enabled',
},
}))
return lookupOrderById(getState(), orderId)
}
const increaseInventoryCountIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
const count = orderProduct.count + 1
await dispatch(updateProductCount({
orderId: order.id,
productId: orderProduct.id,
count,
}))
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const decreaseInventoryCountIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
let orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
const count = orderProduct.count - 1
if (count > 0) {
await dispatch(updateProductCount({
orderId: order.id,
productId: orderProduct.id,
count,
}))
} else {
await dispatch(removeProduct({
orderId: order.id,
productId: orderProduct.id,
}))
}
orderProduct = lookupOrderProduct(getState(), storeId, inventoryId)
}
return orderProduct
}
const removeInventoryFromCartIfNeeded = (storeId, inventoryId) => async (dispatch, getState) => {
const state = getState()
const store = lookupStoreById(state, storeId)
const order = lookupOrder(state, storeId)
const orderProduct = lookupOrderProduct(state, storeId, inventoryId)
if (store && store.orders_enabled && order && orderProduct) {
await dispatch(removeProduct({
orderId: order.id,
productId: orderProduct.id,
}))
}
return null
}
export const UPDATE_STORE_FILTERS = 'UPDATE_STORE_FILTERS'
export const CLEAR_STORE_FILTERS = 'CLEAR_STORE_FILTERS'
export const STORE_FILTERS_KEY = 'storeFilters'
export function updateStoreFilters({ key, filters }) {
return {
type: UPDATE_STORE_FILTERS,
payload: {
key,
filters,
},
}
}
export function clearStoreFilters({ keys }) {
return {
type: CLEAR_STORE_FILTERS,
payload: {
keys,
},
}
}
export const ADD_ADDRESS = 'ADD_ADDRESS'
export const REMOVE_ADDRESS = 'REMOVE_ADDRESS'
export const SELECT_ADDRESS = 'SELECT_ADDRESS'
export function addAddress({ id, name, phone, zip, state, city, street }) {
return {
type: ADD_ADDRESS,
payload: {
id,
name,
phone,
zip,
state,
city,
street,
},
}
}
export function removeAddress(id) {
return {
type: REMOVE_ADDRESS,
payload: {
id,
},
}
}
export function selectAddress(id) {
return {
type: SELECT_ADDRESS,
payload: {
id,